using System; using System.IO; using iTextSharp.text; using iTextSharp.text.pdf; namespace iTextSharpTests { public class PdfWriterPageEvents : PdfPageEventHelper { PdfContentByte _pdfContentByte; // عدد نهایی تعداد کل صفحات را در این قالب قرار خواهیم داد PdfTemplate _template; Font _font; public override void OnOpenDocument(PdfWriter writer, Document document) { FontFactory.Register(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf"); _font = FontFactory.GetFont("Tahoma", BaseFont.IDENTITY_H, embedded: true, size: 9); _pdfContentByte = writer.DirectContent; _template = _pdfContentByte.CreateTemplate(50, 50); } public override void OnEndPage(PdfWriter writer, Document document) { base.OnEndPage(writer, document); var pageSize = document.PageSize; var text = "صفحه " + writer.PageNumber + " از "; var textLen = _font.BaseFont.GetWidthPoint(text, _font.Size); var center = (pageSize.Left + pageSize.Right) / 2; ColumnText.ShowTextAligned( _pdfContentByte, Element.ALIGN_RIGHT, new Phrase(text, _font), center, pageSize.GetBottom(25), 0, PdfWriter.RUN_DIRECTION_RTL, 0); //در پایان هر صفحه یک جای خالی را مخصوص تعداد کل صفحات رزرو خواهیم کرد _pdfContentByte.AddTemplate(_template, center - textLen, pageSize.GetBottom(25)); } public override void OnCloseDocument(PdfWriter writer, Document document) { base.OnCloseDocument(writer, document); _template.BeginText(); _template.SetFontAndSize(_font.BaseFont, _font.Size); _template.SetTextMatrix(0, 0); //درج تعداد کل صفحات در تمام قالبهای اضافه شده _template.ShowText((writer.PageNumber - 1).ToString()); _template.EndText(); } } public class AddTotalNoPages { public static void CreateTestPdf() { using (var pdfDoc = new Document(PageSize.A4)) { var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("tpn.pdf", FileMode.Create)); pdfWriter.PageEvent = new PdfWriterPageEvents(); pdfDoc.Open(); pdfDoc.Add(new Phrase("Page1")); pdfDoc.NewPage(); pdfDoc.Add(new Phrase("Page2")); pdfDoc.NewPage(); pdfDoc.Add(new Phrase("Page3")); } System.Diagnostics.Process.Start("tpn.pdf"); } } }
خطای property not found
var dataSource = new List<WeekDayClassSessionSemiRow>(); for (int i = 0; i < classesList.Count; i++) { int clsId = classesList[i].Id; var sessions = sessionList.Where(p => p.Class_Id == clsId).ToList(); for (int w = 0; w < 17; w++) { var row = new WeekDayClassSessionSemiRow(); row.Class = classesList[i]; row.ClassName = classesList[i].Name; row.ClassFullName = classesList[i].Name + "( " + classesList[i].EducationalPlace.Name + " )"; row.IsSelected = true; row.RowNumber = (w + 1).ToString(); row.WeekNumber = w + 1; row.WeekTitle = "هفته " + (w + 1); row.WD0 = createCell(sessionList, clsId, w + 1, 0); row.WD1 = createCell(sessionList, clsId, w + 1, 1); row.WD2 = createCell(sessionList, clsId, w + 1, 2); row.WD3 = createCell(sessionList, clsId, w + 1, 3); row.WD4 = createCell(sessionList, clsId, w + 1, 4); row.WD5 = createCell(sessionList, clsId, w + 1, 5); row.WD6 = createCell(sessionList, clsId, w + 1, 6); dataSource.Add(row); } }
private WeekDayClassSessionSemi createCell(List<Session> sessionList, int classId, int weekNumber, int day) { var sessions = sessionList .Where(p => p.Class_Id == classId && p.Day == day && p.Week == weekNumber) .ToList(); var ss = new WeekDayClassSessionSemi(); ss.DayNumber = day; ss.HasSession = sessions.Count > 0; int minuteSum = sessions.Sum(p => p.TimeBaseOnMinute); ss.Percent = (minuteSum * 100) / 720; ss.Date = getDateFromCalander(weekNumber, day); ss.SemiDate = ss.Date.Length == 10 ? ss.Date.Substring(5, 5) : ""; ss.IsHoliday = isHoliday(ss.Date); ss.Text = "";// makeFillText(sessionList); ss.WeekNumber = weekNumber + 1; return ss; }
var rpt = new Rpt_ClassPrograms(dataSource, reportParameter); IPdfReportData rptData = rpt.Create();
ابتدا پس از build، فایل appsettings.json را در کنار خروجی برنامه کپی میکنیم (از پوشهی وب، به پوشهی bin\Debug\netcoreapp2.0 تست جاری)
<Target Name="CopyConfig" AfterTargets="AfterBuild"> <Copy SourceFiles="..\MyWebApp\appsettings.json" DestinationFolder="$(OutDir)" /> </Target>
<ItemGroup> <None Update="..\MyWebApp\appsettings.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup>
سپس جائیکه ServiceCollection اولیه آزمونها ساخته میشود، محل این فایل کپی شده را معرفی میکنیم:
var services = new ServiceCollection(); var configuration = new Microsoft.Extensions.Configuration.ConfigurationBuilder() .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false) .AddInMemoryCollection(new[] { new KeyValuePair<string,string>("UseInMemoryDatabase", "true"), }) .Build(); services.AddSingleton<IConfigurationRoot>(provider => configuration);
من همیشه در مورد EF یک نظری داشتم و آن اینست که با اینکه یک ORM، یک هدف مهم را در نظر دارد و آن اینست که تا حد ممکن استانداردهایی را که بین تمامی دیتابیسها مشترک است، رعایت کند، ولی باز قابل قبول است اگر بگوییم که کاربران EF انتظار داشته باشند تا اطلاعات بیشتری در مورد sql server در آن نهفته باشد. از یک سو هر دو محصول مایکروسافت هستند و از سوی دیگر مطمئنا توسعه گران محصولات دات نت بیش از هر چیزی به sql server نگاه ویژهتری دارند. پس مایکروسافت در کنار حفظ آن ویژگیهای مشترک، باید به حفظ استانداردهای جدایی برای sql server هم باشد.
تعدادی از برنامه نویسان در هنگام ایجاد Domain Model کم لطفیهای زیادی را میکنند که یکی از آنها عدم کنترل نوع دادههای خود است. مثلا برای رشتهها هیچ محدودیتی را در نظر نمیگیرند. شاید در سمت کلاینت اینکار را انجام میدهند؛ ولی نکتهی مهم در طرف دیتابیس است که چگونه تعریف میشود. در این حالت (nvarchar(MAX در نظر گرفته میشود که به معنی اشاره به منطقه دوگیگابایتی از اطلاعات است. در نکات بعدی، قصد داریم این مرحله را یک گام به جلوتر پیش ببریم و آن هم ایجاد نوع دادههای بهینهتر در Sql Server است.
نکته مهم: بدیهی است که تغییرات زیر، ORM شما را تنها به sql server مقید میکند که بعدها در صورت تغییر دیتابیس نیاز به حذف موارد زیر را خواهید داشت؛ در غیر اینصورت به مشکل عدم ایجاد دیتابیس برخواهید خورد.
اولین مورد مهم بحث تاریخ و زمان است؛ وقتی ما یک نوع داده را تنها DateTime در نظر بگیریم، در Sql Server هم همین نوع داده وجود دارد و انتخاب میشود. ولی اگر شما واقعا نیازی به این نوع داده نداشته باشید چطور؟ در حال حاضر من بر روی یک برنامهی کارخانه کار میکنم که بخش کارمندان و گارگران آن سه داده زمانی زیر را شامل میشود:
public DateTime BirthDate { get; set; } public DateTime HireDate { get; set; } public DateTime? LeaveDate { get; set; }
حال به جدول زیر نگاه کنید که هر نوع داده چه مقدار فضا را به خود اختصاص میدهد:
SmallDateTime | 4 بایت |
DateTime | 8 بایت |
DateTime2 | 6 تا 8 بایت |
DateTimeOffset | 8 تا 10 بایت |
Date | 3 بایت |
Time | 3 تا 5 بایت |
از این جدول چه میفهمید؟ با یک نگاه میتوان فهمید که ساختار بالای من باید 24 بایت گرفته باشد؛ برای ساختاری که هم تاریخ و هم زمان (ساعت) را پشتیبانی میکند. ولی با نگاه دقیقتر به نام پراپرتیها این نکته روشن میشود که ما یک گپ Gap (فضای بیهوده) داریم چون زمان تولد، استخدام و ترک سازمان اصلا نیازی به ساعت ندارند و همان تاریخ کافی است. یعنی نوع Date با حجم کلی 9 بایت؛ که در نتیجه 15 بایت صرفه جویی در یک رکورد صورت خواهد گرفت.
پس کد بالا را به شکل زیر تغییر میدهم:
[Column(TypeName = "date")] public DateTime BirthDate { get; set; } [Column(TypeName = "date")] public DateTime HireDate { get; set; } [Column(TypeName = "date")] public DateTime? LeaveDate { get; set; }
خصوصیت Column از نسخه 4.5دات نت فریم ورک اضافه شده و در فضای نام System.ComponentModel.DataAnnotations.Schema قرار گرفته است.نوعهایی که در بالا با سایز متغیر هستند، به نسبت دقتی که برای آن تعیین میکنید، سایز میگیرند. مثل (time(0 که 3 بایت از حافظه را میگیرد. در صورتی که time معرفی کنید، به جای اینکه از شیء DateTime استفاده کنید، از شی Timespan استفاده کنید، تا در پشت صحنه از نوع داده time استفاده کند. در این حالت حداکثر حافظه یعنی 5 بایت را برخواهد داشت و بهترین حالت ممکن این هست که نیاز خود را بسنجید و خودتان دقت آن را مشخص کنید. دو شکل زیر نحوهی تعریف نوع زمان را مشخص میکنند. یکی حالت پیش فرض و دیگری انتخاب دقت:
public class Testtypes { public TimeSpan CloseTime { get; set; } public TimeSpan CloseTime2 { get; set; } } public class TestConfig : EntityTypeConfiguration<Testtypes> { public TestConfig() { this.Property(x => x.CloseTime2).HasPrecision(3); } }
مورد دوم در مورد دادههای اعشاری است:
float flExample=23.2f;
//real public float FloatData { get; set; } //real public Single SingleData { get; set; } //float public double DoubleData { get; set; }
4235.254
decimal d1=4545.112m;
modelBuilder.Entity<Class>().Property(object => object.property).HasPrecision(7, 3);
public class Testtypes { public Decimal Decimal1 { get; set; } public Decimal Decimal2 { get; set; } } public class TestConfig : EntityTypeConfiguration<Testtypes> { public TestConfig() { this.Property(x => x.Decimal2).HasPrecision(7, 3); } }
مورد سوم مبحث رشته هاست:
[StringLength(25)] public string FirstName { get; set; } [StringLength(30)] [Column(TypeName = "varchar")] public string EnProductTitle { get; set; } public string ArticleContent { get; set; } [Column(TypeName = "varchar(max)")] public string ArticleContentEn { get; set; }
this.Property(e => e.EnProductTitle).HasColumnType("VARCHAR").HasMaxLength(30);
modelBuilder.Properties<string>().Configure(c => c.HasColumnType("varchar")); //=========== یا modelBuilder.Properties<string>().Configure(c => c.IsUnicode(false));
//tinyInt public byte Age { get; set; } //smallInt public Int16 OldInt { get; set; } //int public int Int32 { get; set; } //Bigint public Int64 HighNumbers { get; set; }
• Next : یک عدد تصادفی را برای ما تولید میکند.
• NextByte : آرایهای از بایتها را که با اعداد تصادفی پر شدهاند تولید میکند.
• NextDouble : یک عدد تصادفی را بین 0.0 و 1.0 باز میگرداند.
بررسی متد Next
متد Next سه Overload مختلف دارد و این امکان را برای شما مهیا میکند تا 2 عدد را به عنوان بازه تولید اعداد تصادفی انتخاب کنید (حد پایین و بالای بازه).
تولید یک عدد تصادفی:
var rand = new Random().Next();
var rand = new Random().Next(1000);
کد زیر عددی تصادفی را بین محدوده تعیین شده تولید میکند:
public static int RandomNumber(int min, int max) { var rand = new Random(); return rand.Next(min, max); }
بررسی متد NextDouble
قطعه کد زیر (به کمک توابع کلاس Random) رشتهای تصادفی را با طول مشخصی برای ما تولید میکند؛ با قابلیت تعیین بزرگ و یا کوچک بودن کاراکترهای رشته تصادفی:
public static string RandomString(int size, bool lowerCase) { StringBuilder builder = new StringBuilder(); Random random = new Random(); char ch; for (int i = 0; i < size; i++) { //تولید عدد و تبدیل آن به کاراکتر ch = Convert.ToChar(Convert.ToInt32(Math.Floor(26 * random.NextDouble() + 65))); builder.Append(ch); } if (lowerCase) return builder.ToString().ToLower(); return builder.ToString(); }
public string RandomPassword() { StringBuilder builder = new StringBuilder(); builder.Append(RandomString(4, true)); builder.Append(RandomNumber(1000, 9999)); builder.Append(RandomString(2, false)); return builder.ToString(); }
همانطور که در ابتدای مطلب اشاره شد، خروجی این تابع آرایهای از بایتها میباشد و هر خانهی آن عددی است تصادفی که بزرگتر و یا مساوی 0 و کوچکتر از مقدار Maximum نوع داده Byte است. کد زیر نحوه استفاده از این تابع را نشان میدهد:
byte[] b = new byte[10]; Random rnd = new Random(); rnd.NextBytes(b); for (int i = 0; i < b.Length; i++) { Console.WriteLine(b[i]); }
153 115 86 5 161 190 249 228
... var token = await _tokenStoreService.FindTokenAsync(refreshTokenValue); ...
... var refreshTokenSerial = _tokenFactoryService.GetRefreshTokenSerial(refreshTokenValue); ...
... decodedRefreshTokenPrincipal = new JwtSecurityTokenHandler().ValidateToken( refreshTokenValue, new TokenValidationParameters { RequireExpirationTime = true, ValidateIssuer = false, ValidateAudience = false, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Value.Key)), ValidateIssuerSigningKey = true, // verify signature to avoid tampering ValidateLifetime = true, // validate the expiration ClockSkew = TimeSpan.Zero // tolerance for the expiration date }, out _ ); ...
چند نکته اضافه برای Refactoring
public bool SaveTextF(string path) { if (_isTextChanged) { if (string.IsNullOrWhiteSpace(path)) { using (var dlg = new SaveFileDialog {Filter = "*.txt"}) { if (dlg.ShowDialog() == DialogResult.OK) path = dlg.FileName; else return false; } } //call save methods on path } return true; }
public bool SaveTextT(string path) { if (!_isTextChanged) return true; if (string.IsNullOrWhiteSpace(path)) using (var dlg = new SaveFileDialog {Filter = "*.txt"}) { if (dlg.ShowDialog() != DialogResult.OK) return false; path = dlg.FileName; } //call save methods on path return true; }
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت چهاردهم- آماده شدن برای انتشار برنامه
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); IdentityModelEventSource.ShowPII = true; services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("Cookies", options => { options.AccessDeniedPath = "/Authorization/AccessDenied"; // set session lifetime options.ExpireTimeSpan = TimeSpan.FromHours(8); // sliding or absolute options.SlidingExpiration = false; // host prefixed cookie name options.Cookie.Name = "MVC"; // strict SameSite handling options.Cookie.SameSite = SameSiteMode.Strict; }) .AddOpenIdConnect("oidc", options => { options.SignInScheme = "Cookies"; options.Authority = Configuration["IDPBaseAddress"]; options.ClientId = Configuration["ClientId"]; options.ClientSecret = Configuration["ClientSecret"]; options.ResponseType = "code id_token"; options.ResponseMode = "query"; options.RequireHttpsMetadata = false; options.CallbackPath = new PathString("/Home/"); options.SignedOutCallbackPath = new PathString("/Home/"); options.MapInboundClaims = true; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("roles"); options.Scope.Add("PS.WebApi.Read"); options.Scope.Add("offline_access"); options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; //options.UsePkce = true; //options.ClaimActions.MapJsonKey(claimType: "role", jsonKey: "role"); // for having 2 or more roles options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.GivenName, RoleClaimType = JwtClaimTypes.Role }; }); //ServicePointManager.Expect100Continue = true; //ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls // | SecurityProtocolType.Tls11 // | SecurityProtocolType.Tls12 // | SecurityProtocolType.Ssl3; } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "areas", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}" ); //.RequireAuthorization(); endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}" ); //.RequireAuthorization(); }); //[HttpPost] //public IActionResult Logout() //{ // return SignOut("Cookies", "oidc"); //} }
پروژههای بنده به ترتیب Endpoint هاشون اینگونه هست:
"IDPBaseAddress": "https://localhost:44310",
"ClientId": "Mvc_ClientId",
"ClientSecret": "WebMvc"
{ "IdentityServerData": { "IdentityResources": [ { "Name": "roles", "Enabled": true, "DisplayName": "Roles", "UserClaims": [ "role" ] }, { "Name": "openid", "Enabled": true, "Required": true, "DisplayName": "Your user identifier", "UserClaims": [ "sub" ] }, { "Name": "profile", "Enabled": true, "DisplayName": "User profile", "Description": "Your user profile information (first name, last name, etc.)", "Emphasize": true, "UserClaims": [ "name", "family_name", "given_name", "middle_name", "nickname", "preferred_username", "profile", "picture", "website", "gender", "birthdate", "zoneinfo", "locale", "updated_at" ] }, { "Name": "email", "Enabled": true, "DisplayName": "Your email address", "Emphasize": true, "UserClaims": [ "email", "email_verified" ] }, { "Name": "address", "Enabled": true, "DisplayName": "Your address", "Emphasize": true, "UserClaims": [ "address" ] } ], "ApiScopes": [ { "Name": "Idp_Admin_ClientId_api", "DisplayName": "Idp_Admin_ClientId_api", "Required": true, "UserClaims": [ "role", "name" ] }, { "Name": "WebApi.Read", "DisplayName": "WebApi Read", "Required": true, "UserClaims": [ "role", "WebApi.Read" ] }, { "Name": "WebApi.Write", "DisplayName": "WebApi Write", "Required": true, "UserClaims": [ "role", "WebApi.Write" ] } ], "ApiResources": [ { "Name": "Idp_Admin_ClientId_api", "Scopes": [ "Idp_Admin_ClientId_api" ] }, { "Name": "WebApi", "Scopes": [ "WebApi.Read", "WebApi.Write" ] } ], "Clients": [ { "ClientId": "Idp_Admin_ClientId", "ClientName": "Idp_Admin_ClientId", "ClientUri": "https://localhost:44303", "AllowedGrantTypes": [ "authorization_code" ], "RequirePkce": true, "ClientSecrets": [ { "Value": "Idp_Admin_ClientSecret" } ], "RedirectUris": [ "https://localhost:44303/signin-oidc" ], "FrontChannelLogoutUri": "https://localhost:44303/signout-oidc", "PostLogoutRedirectUris": [ "https://localhost:44303/signout-callback-oidc" ], "AllowedCorsOrigins": [ "https://localhost:44303" ], "AllowedScopes": [ "openid", "email", "profile", "roles" ] }, { "ClientId": "Idp_Admin_ClientId_api_swaggerui", "ClientName": "Idp_Admin_ClientId_api_swaggerui", "AllowedGrantTypes": [ "authorization_code" ], "RequireClientSecret": false, "RequirePkce": true, "RedirectUris": [ "https://localhost:44302/swagger/oauth2-redirect.html" ], "AllowedScopes": [ "Idp_Admin_ClientId_api" ], "AllowedCorsOrigins": [ "https://localhost:44302" ] }, //WebApi { "ClientId": "WebApi_ClientId", "ClientName": "WebApi_ClientId", "ClientUri": "https://localhost:44365", "AllowedGrantTypes": [ "authorization_code" ], "RequirePkce": true, "ClientSecrets": [ { "Value": "WebApi" } ], "RedirectUris": [ "https://localhost:44303/signin-oidc" ], "FrontChannelLogoutUri": "https://localhost:44303/signout-oidc", "PostLogoutRedirectUris": [ "https://localhost:44303/signout-callback-oidc" ], "AllowedCorsOrigins": [ "https://localhost:44303", "https://localhost:44310" ], "AllowedScopes": [ "openid", "email", "profile", "roles" ] }, //Mvc { "ClientId": "Mvc_ClientId", "ClientName": "Mvc_ClientId", "ClientUri": "https://localhost:44332", "AllowedGrantTypes": [ "hybrid" ], //"RequirePkce": true, "AllowPlainTextPkce": false, "ClientSecrets": [ { "Value": "WebMvc" } ], "RedirectUris": [ "https://localhost:44332/signin-oidc" ], "FrontChannelLogoutUri": "https://localhost:44332/signout-oidc", "PostLogoutRedirectUris": [ "https://localhost:44332/signout-callback-oidc" ], "AllowedCorsOrigins": [ "https://localhost:44332", "https://localhost:44310" ], "AllowedScopes": [ "openid", "email", "profile", "roles", "address", "PS.webApi" ], "AllowAccessTokensViaBrowser": true, "RequireConsent": false, "AllowOfflineAccess": true //"UpdateAccessTokenClaimsOnRefresh": true } ] } }
public class HomeController : Controller { private readonly ILogger<HomeController> _logger; public HomeController(ILogger<HomeController> logger) { _logger = logger; } public IActionResult Default() { return View(); } public IActionResult Index() { return View(); } [Authorize] public IActionResult Privacy() { return View(); } [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } }
اما در نهایت بعد از اجرا و مراجعه به آدرس https://localhost:44332/home/privacy که مزین به اتریبیوت
[Authorize]میباشد با خطای زیر مواجه میشوم:
لازم به توضیح هست که پروپرتی RequireHttpsMetadata = false میباشد.
[BreezeController] public class AccountController : ApiController { ... }
module Interfaces { export interface IAuthService { user: Models.IUserToken getUserInfo(accessToken); login(data); logOut(); register(data); changePassword(data); accessToken(accessToken, data); } }
سرویس AuthService پیاده سازی اینترفیس IAuthService را برعهده دارد. در سازنده آن، وابستگیهای آن مقداردهی شدهاست و همچنین تنظیمات manager را انجام دادهایم.
"grant_type=password & username=myusername & password=mypassword";
var ajaxAdapter = breeze.config.getAdapterInstance("ajax"); breeze.ajaxpost(ajaxAdapter);
.withParameters({ $method: 'POST', $encoding: 'JSON', $data: newData }
module AdApps { var securityUrls = { site: '/', login: '/token', logout: 'logout', register: 'register', userInfo: 'getUserInfo', changePassword: 'changePassword', } export class AuthService implements Interfaces.IAuthService { private manager: breeze.EntityManager; constructor( private _breeze: typeof breeze, private $http: ng.IHttpProvider, private toaster: ngtoaster.IToasterService, private $location: ng.ILocationService) { var dataService = new _breeze.DataService({ serviceName: "/breeze/Account", hasServerMetadata: false }); var metadataStore = new _breeze.MetadataStore({ namingConvention: _breeze.NamingConvention.camelCase }); this.manager = new _breeze.EntityManager({ dataService: dataService, metadataStore: metadataStore, saveOptions: new _breeze.SaveOptions({ allowConcurrentSaves: true, tag: [{}] }) }); } user: Models.IUserToken; accessToken(accessToken, data): string { if (accessToken === 'clear') { localStorage.removeItem('accessToken'); delete this.$http.defaults.headers.common.Authorization; } else { window.localStorage.setItem("accessToken", accessToken); this.$http.defaults.headers.common.Authorization = 'Bearer ' + accessToken; } return accessToken; } getUserInfo(): ng.IPromise<any> { var query = this._breeze.EntityQuery.from(securityUrls.userInfo); return this.manager.executeQuery(query).then(data => { return data.results[0]; }); } login(data: any): ng.IPromise<any> { var newData = "grant_type=password&username=" + data.userName + "&password=" + data.password; var query = this._breeze.EntityQuery.from(securityUrls.login) .withParameters({ $method: 'POST', $encoding: 'JSON', $data: newData }); return this.manager.executeQuery(query).then(data => { var self = this; var result = data.results[0] as any; self.accessToken(result.access_token, data.results[0]); self.user = <Models.IUserToken>{}; self.user = <Models.IUserToken>result; return result; }); } logOut(): ng.IPromise<any> { var query = this._breeze.EntityQuery.from(securityUrls.logout) .withParameters({ $method: 'POST', $encoding: 'JSON', }); return this.manager.executeQuery(query).then(data => { this.user = null; this.accessToken('clear', null); this.$location.path("/"); }); } register(data: Object): ng.IPromise<any> { var query = this._breeze.EntityQuery.from(securityUrls.register) .withParameters({ $method: 'POST', $encoding: 'JSON', $data: data }); return this.manager.executeQuery(query).then(data => { }); } changePassword(data: Object): ng.IPromise<any> { var query = this._breeze.EntityQuery.from(securityUrls.changePassword) .withParameters({ $method: 'POST', $encoding: 'JSON', $data: data }); return this.manager.executeQuery(query).then(data => { }); } } }
سرویس HttpInterceptor : رهگیری و پیگیری کردن نتیجه درخواستهای http را بر عهده دارد.
module AdApps { export class HttpInterceptor { private static _toaster: ngtoaster.IToasterService; private static _$q: ng.IQService; constructor( private $q: ng.IQService, private toaster: ngtoaster.IToasterService, private $location: ng.ILocationService) { HttpInterceptor._toaster = toaster; HttpInterceptor._$q = $q; } request(config): string { config.headers = config.headers || {}; var authData = window.localStorage.getItem("accessToken"); if (authData) { config.headers.Authorization = "Bearer " + authData; } return config; }; response(response): ng.IPromise<any> { if (response.data && response.data.message && response.status === 200) { HttpInterceptor._toaster.success(response.data.message) } return HttpInterceptor._$q.resolve(response); }; responseError(response): ng.IPromise<any> { var self = this; var data = response.data; var title = "خطا"; var messages = []; if (data) { if (data.error) { title = data.error; } if (data.message) { messages.push(data.message); } if (data.Message) { messages.push(data.Message); } if (data.ModelState) { angular.forEach(data.ModelState, function (errors, key) { if (key.substr(0, 1) != "$") { messages.push(errors); } }); } if (data.exceptionMessage) { messages.push(data.exceptionMessage); } if (data.ExceptionMessage) { messages.push(data.ExceptionMessage); } if (data.error_description) { messages.push(data.error_description); } if (messages.length > 0) { HttpInterceptor._toaster.error(title, messages.join("<br/>")); } if (response.status === "401") { self.$location.path("/ورود"); } } return HttpInterceptor._$q.reject(response); } } }
معرفی کردن مسیرهای ورود، ثبت نام و تغییر رمز عبور به انگولار
module AdApps { class SecurityCtrl { constructor(private $scope: Interfaces.IAuthScope, private authService: AuthService) { $scope.authService = authService; if (window.localStorage.getItem("accessToken") != null) { authService.getUserInfo().then(function (data) { $scope.authService.user = data; }); } $scope.logOut = function () { return authService.logOut().then(function () { }); } } } define(["angularAmd", "angular", "factory/AuthService", "factory/httpInterceptor"], (angularAmd, ng) => { angularAmd = angularAmd.__proto__; var app = ng.module("AngularTypeScript", ['ngRoute', 'breeze.angular', 'toaster']); var viewPath = "app/views/"; var controllerPath = "app/controller/"; app.config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) { $httpProvider.interceptors.push("HttpInterceptor"); $routeProvider .when("/", angularAmd.route({ templateUrl: viewPath + "home.html", controllerUrl: controllerPath + "home.js" })) .when("/login", angularAmd.route({ templateUrl: viewPath + "login.html", controllerUrl: controllerPath + "login.js" })) .when("/register", angularAmd.route({ templateUrl: viewPath + "register.html", controllerUrl: controllerPath + "register.js" })) .when("/changePassword", angularAmd.route({ templateUrl: viewPath + "change-password.html", controllerUrl: controllerPath + "changePassword.js" })) .otherwise({ redirectTo: '/' }); } ]); app.service('AuthService', ['breeze', '$http', 'toaster', '$location', AuthService]); app.service("HttpInterceptor", ["$q", "toaster", "$location", HttpInterceptor]); app.controller('SecurityCtrl', ['$scope', 'AuthService', SecurityCtrl]); return angularAmd.bootstrap(app); }); }
ایجاد کنترلر .login.ts و ارسال سرویسهای لازم به کلاس LoginCtrl
در صورت صحیح بودن نام کاربری و رمز عبور به صفحه اصلی هدایت خواهد شد.
module AdApps { define(['app'], function (app) { app.controller('LoginCtrl', ["$scope", "AuthService", "$location", LoginCtrl]); }); export class LoginCtrl { constructor($scope: Interfaces.ILoginScope, authService: AuthService, $location: ng.ILocationService) { $scope.submit = function () { authService.login(angular.copy($scope.form)) .then(function (data) { this.$location.path("/"); }) }; } } }
ایجاد login.html
<div ng-controller="LoginCtrl"> <div> <i></i> <span>ورود</span> <div> <div> </div> </div> </div> <div> <div> <div> <form name="Form" id="form1"> <fieldset> <div> <div> <input name="username" ng-model="form.userName" placeholder="نام کاربری" required> <span> <i></i> </span> </div> </div> <div> <div> <input name="password" type="password" ng-model="form.password" placeholder="{{'Password'}}" validator="required"> <span> <i></i> </span> </div> </div> </fieldset> <div> <button type="submit" ng-click="submit()">ورود</button> </div> </form> </div> </div> </div> </div>
requirejs.config({ paths: { "app": "app", "angularAmd": "/Scripts/angularAmd", "angular": "/Scripts/angular", "breezeAjaxpost": "/Scripts/breeze/breeze.ajaxpost", "breeze": "/Scripts/breeze/breeze.debug", "breezeAngular": "/Scripts/breeze/breeze.angular", "bootstrap": "/Scripts/bootstrap", "angularRoute": "/Scripts/angular-route", "jquery": "/Scripts/jquery-2.2.2", "entityManagerService": "factory/entityManagerService", "toaster": "/Scripts/toaster", }, waitSeconds: 0, shim: { "angular": { exports: "angular" }, "angularRoute": { deps: ["angular"] }, "bootstrap": { deps: ["jquery"] }, "breeze": { deps: ["jquery"] }, "breezeAngular": { deps: ["angular", "breeze"] }, "toaster": { deps: ["angular"] }, "app": { deps: ["bootstrap", "angularRoute", "toaster", "breezeAngular", "breezeAjaxpost"] } } }); require(["app"]);
ایجاد ساختار ابتدایی پروژه
برای ساخت پروژه، به خط فرمان مراجعه کرده و با دستور زیر، یک پروژهی react از نوع typescript را ایجاد میکنیم.
npx create-react-app todo-mobx --template typescript cd todo-mobx
برای توسعهی این مثال، از محیط توسعهی VSCode استفاده میکنیم. اگر VSCode بر روی سیستم شما نصب باشد، در همان مسیری که خط فرمان باز است، دستور زیر را اجرا کنید؛ پروژهی شما در VSCode باز میشود:
code
سپس در محیط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:
npm install --save-dev typescript @types/node @types/react @types/react-dom @types/jest npm install mobx mobx-react-lite --save
در ادامه برای استایل بندی بهتر برنامه از کتابخانههای bootstrap و font-awesome استفاده میکنیم:
npm install bootstrap --save npm install font-awesome --save
سپس فایل index.tsx را باز کرده و دو خط زیر را به آن اضافه میکنیم:
import "bootstrap/dist/css/bootstrap.css"; import "font-awesome/css/font-awesome.css";
کتابخانهی MobX، از تزئین کنندهها یا decorators استفاده میکند. بنابراین نیاز است به tsconfig پروژه مراجعه کرده و خط زیر را به آن اضافه کنیم:
"compilerOptions": { .... , "experimentalDecorators": true }
ایجاد مخازن حالت MobX
در ادامه نیاز است storeهای MobX را ایجاد کنیم و بعد آنها را به react اتصال دهیم. بدین منظور یک پوشهی جدید را در مسیر src، به نام stores ایجاد میکنیم و سپس فایل جدیدی را به نام todo-item.ts در آن با محتوای زیر ایجاد میکنیم:
import { observable, action } from "mobx"; export default class TodoItem { id = Date.now(); @observable text: string = ''; @observable isDone: boolean = false; constructor(text: string) { this.text = text; } @action toggleIsDone = () => { this.isDone = !this.isDone } @action updateText = (text: string) => { this.text = text; } }
در همان مسیر stores، فایل دیگری را نیز به نام todo-list.ts، با محتوای زیر ایجاد میکنیم:
import { observable, computed, action } from "mobx"; import TodoItem from "./todo-item"; export class TodoList { @observable.shallow list: TodoItem[] = []; constructor(todos: string[]) { todos.forEach(this.addTodo); } @action addTodo = (text: string) => { this.list.push(new TodoItem(text)); } @action removeTodo = (todo: TodoItem) => { this.list.splice(this.list.indexOf(todo), 1); }; @computed get finishedTodos(): TodoItem[] { return this.list.filter(todo => todo.isDone); } @computed get openTodos(): TodoItem[] { return this.list.filter(todo => !todo.isDone); } }
توضیحات:
مفهوم observable@: کل شیء state را به صورت یک شیء قابل ردیابی JavaScript ای ارائه میکند.
مفهوم computed@: این نوع خواص، مقدار خود را زمانیکه observableهای وابستهی به آنها تغییر کنند، به روز رسانی میکنند.
مفهوم action@: جهت به روز رسانی state و سپس نمایش تغییرات یا نمایش نمونهی دیگری در DOM میباشند.
import { createContext, useContext } from "react"; import { TodoList } from "../stores/todo-list"; export const StoreContext = createContext<TodoList>({} as TodoList); export const StoreProvider = StoreContext.Provider; export const useStore = (): TodoList => useContext(StoreContext);
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; import "bootstrap/dist/css/bootstrap.css"; import "font-awesome/css/font-awesome.css"; import { TodoList } from './stores/todo-list'; import { StoreProvider } from './providers/store-provider'; const todoList = new TodoList([ 'Read Book', 'Do exercise', 'Watch Walking dead series' ]); ReactDOM.render( <StoreProvider value={todoList}> <App /> </StoreProvider> , document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
import React, { useState } from 'react'; import { useStore } from '../providers/store-provider'; export const TodoNew = () => { const [newTodo, setTodo] = useState(''); const todoList = useStore(); const addTodo = () => { todoList.addTodo(newTodo); setTodo(''); }; return ( <div className="input-group mb-3"> <input type="text" className="form-control" placeholder="Add To do" value={newTodo} onChange={(e) => setTodo(e.target.value)} /> <div className="input-group-append"> <button className="btn btn-success" type="submit" onClick={addTodo}>Add Todo</button> </div> </div> ) };
import React from 'react'; import { TodoItem } from "./TodoItem"; import { useObserver } from "mobx-react-lite"; import { useStore } from '../providers/store-provider'; export const TodoList = () => { const todoList = useStore(); return useObserver(() => ( <div> <h1>Open Todos</h1> <table className="table"> <thead className="thead-dark"> <tr> <th>Name</th> <th className="text-left">Do It?</th> <th>Actions</th> </tr> </thead> <tbody> { todoList.openTodos.map(todo => <tr key={`${todo.id}-${todo.text}`}> <TodoItem todo={todo} /> </tr>) } </tbody> </table> <h1>Finished Todos</h1> <table className="table"> <thead className="thead-light"> <tr> <th>Name</th> <th className="text-left">Do It?</th> <th>Actions</th> </tr> </thead> <tbody> { todoList.finishedTodos.map(todo => <tr key={`${todo.id}-${todo.text}`}> <TodoItem todo={todo} /> </tr>) } </tbody> </table> </div> )); };
import React, { useState } from 'react'; import TodoItemClass from "../stores/todo-item"; import { useStore } from '../providers/store-provider'; interface Props { todo: TodoItemClass; } export const TodoItem = ({ todo }: Props) => { const todoList = useStore(); const [newText, setText] = useState(''); const [isEditing, setEdit] = useState(false); const saveText = () => { todo.updateText(newText); setEdit(false); setText(''); }; return ( <React.Fragment> { isEditing ? <React.Fragment> <td> <input className="form-control" placeholder={todo.text} type="text" onChange={(e) => setText(e.target.value)} /> </td> <td></td> <td> <button className="btn btn-xs btn-success " onClick={saveText}>Save</button> </td> </React.Fragment> : <React.Fragment> <td> {todo.text} </td> <td className="text-left"> <input className="form-check-input" type="checkbox" onChange={todo.toggleIsDone} defaultChecked={todo.isDone}></input> </td> <td> <button className="btn btn-xs btn-warning " onClick={() => setEdit(true)}> <i className="fa fa-edit"></i> </button> <button className="btn btn-xs btn-danger ml-2" onClick={() => todoList.removeTodo(todo)}> <i className="fa fa-remove"></i> </button> </td> </React.Fragment> } </React.Fragment> ) };