- اول، ابزار Microsoft ASP.NET Scaffolding را از منوی Tools گزینه Extensions and Updates دریافت و نصب نمایید.
- دوم پروژه جدیدی از نوع Visual C# ASP.NET Web Forms Application با فریم ورک 4.5 ایجاد نمایید.
- از پنجره NuGet Package manager با دستور install کتابخانه ASP.NET Web Forms Scaffold Generator را دریافت نمایید
install-package Microsoft.AspNet.Scaffolding.WebForms -pre
- کلاس Person را مانند زیر در فولدر Models ایحاد نماییدویژگی ScaffoldColumn را برای ID، برابر false قرار دهید تا از ایجاد این ستون جلوگیری نمائید.
public class Person { [ScaffoldColumn(false)] public int ID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } }
- پروژه را Build نمایید.
- بر روی پروژه راست کلیک و از گزینه Add، گزینه ...Scaffold را انتخاب نمایید.
- از پنجره Add Scaffold باز شده بر روی گزینه Add، کلیک کنید.
- پنجره
Add Web Forms Pages مانند زیر باز میشود که امکان انتخاب کلاس،Data Context و MasterPage فراهم میباشد.
- از گزینه Data Context class گزینه New Data Context را انتخاب نمایید. صفحات مورد نیاز را در فولدر Views/Person ایجاد مینمایید.
- کدهای تولید شده را میتوانید بازبینی نمایید پروژه را اجرا تا خروجی کار را مشاهده نمایید.
تامین مقادیر پارامترها در حین نگاشتهای AutoMapper
- جلوگیری از Lazy loading اشتباه
- کاهش تعداد فیلدهای بازگشت داده شدهی از دیتابیس و محدود ساختن آنها به خواصی که قرار است نگاشت شوند. در حالت معمولی استفادهی از متد Mapper.Map، تمام فیلدهای مدل بارگذاری شده و سپس در سمت کلاینت توسط AutoMapper نگاشت خواهند شد. اما در حالت استفادهی از متد ویژهی Project To، کوئری SQL ارسالی به بانک اطلاعاتی نیز مطابق نگاشت تعریف شده، تغییر کرده و خلاصه خواهد شد.
در این حالت یک چنین سناریویی را درنظر بگیرید. مدل متناظر با جدول بانک اطلاعاتی ما چنین ساختاری را دارد:
public class UserModel { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } }
public class UserViewModel { public string FirstName { get; set; } public string LastName { get; set; } public string UserIdentityName { get; set; } }
تعریف نگاشتهای پارامتری
برای حل این مساله، از روش زیر استفاده میشود:
string userIdentityName = null; this.CreateMap<UserModel, UserViewModel>() .ForMember(d => d.UserIdentityName, opt => opt.MapFrom(src => userIdentityName));
اکنون جهت استفادهی از این متغیر با قابلیت جایگزینی، میتوان به نحو ذیل عمل کرد:
var uiUsers = users.AsQueryable() .Project() .To<UserViewModel>(new { userIdentityName = "User.Identity.Name Value Here" }) .ToList();
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
ASP.NET MVC #18
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult LogOn(User user, string returnUrl) { if (this.ModelState.IsValid) { if (_userService.IsValid(user)) { int userID = _userService.GetUser(u => u.Username == user.Username && u.Password == user.Password).Id; FormsAuthentication.SetAuthCookie(userID.ToString(CultureInfo.InvariantCulture), user.RememberMe); if (shouldRedirect(returnUrl)) { return Redirect(returnUrl); } FormsAuthentication.RedirectFromLoginPage(userID.ToString(CultureInfo.InvariantCulture), user.RememberMe); } } this.ModelState.AddModelError("", "The user name or password provided is incorrect."); ViewBag.Error = "Login faild! Make sure you have entered the right user name and password!"; return View(user); }
اینم کد تولید توکنه
public async Task<string> GenerateTokenAsync(ApplicationUser User) { var secretKey = Encoding.UTF8.GetBytes(_siteSettings.JwtSettings.SecretKey); var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature); var encrytionKey = Encoding.UTF8.GetBytes(_siteSettings.JwtSettings.EncrypKey); var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encrytionKey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256); var tokenDescriptor = new SecurityTokenDescriptor() { Issuer = _siteSettings.JwtSettings.Issuer, Audience = _siteSettings.JwtSettings.Audience, IssuedAt = DateTime.Now, NotBefore = DateTime.Now.AddMinutes(_siteSettings.JwtSettings.NotBeforeMinutes), Expires = DateTime.Now.AddMinutes(_siteSettings.JwtSettings.ExpirationMinutes), SigningCredentials = signingCredentials, Subject = new ClaimsIdentity(await GetClaimsAsync(User)), EncryptingCredentials = encryptingCredentials, }; var tokenHandler = new JwtSecurityTokenHandler(); var securityToken = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(securityToken); } public async Task<IEnumerable<Claim>> GetClaimsAsync(ApplicationUser User) { var Claims = new List<Claim>() { new Claim(ClaimTypes.Name,User.UserName), new Claim(ClaimTypes.NameIdentifier,User.Id), new Claim(ClaimTypes.MobilePhone,User.PhoneNumber), new Claim(new ClaimsIdentityOptions().SecurityStampClaimType,User.SecurityStamp), new Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString()) }; var Roles = _roleManager.Roles.ToList(); foreach (var item in Roles) { var RoleClaims = await _roleManager.FindClaimsInRole(item.Id); foreach (var claim in RoleClaims.Claims) { Claims.Add(new Claim(ConstantPolicies.DynamicPermissionClaimType, claim.ClaimValue)); } } foreach (var item in Roles) Claims.Add(new Claim(ClaimTypes.Role, item.Name)); return Claims; }
توی پروژه ما backendبا .netoreو frontهم با react هست
دریافت کتابخانه DNT Scheduler و مثال آن
DNTScheduler
در این بسته، کدهای کتابخانهی DNT Scheduler و یک مثال وب فرم را، ملاحظه خواهید کرد. از این جهت که برای ثبت وظایف این کتابخانه، از فایل global.asax.cs استفاده میشود، اهمیتی ندارد که پروژهی شما وب فرم است یا MVC. با هر دو حالت کار میکند.
نحوهی تعریف یک وظیفهی جدید
کار با تعریف یک کلاس و پیاده سازی ScheduledTaskTemplate شروع میشود:
public class SendEmailsTask : ScheduledTaskTemplate
using System; namespace DNTScheduler.TestWebApplication.WebTasks { public class SendEmailsTask : ScheduledTaskTemplate { /// <summary> /// اگر چند جاب در یک زمان مشخص داشتید، این خاصیت ترتیب اجرای آنها را مشخص خواهد کرد /// </summary> public override int Order { get { return 1; } } public override bool RunAt(DateTime utcNow) { if (this.IsShuttingDown || this.Pause) return false; var now = utcNow.AddHours(3.5); return now.Minute % 2 == 0 && now.Second == 1; } public override void Run() { if (this.IsShuttingDown || this.Pause) return; System.Diagnostics.Trace.WriteLine("Running Send Emails"); } public override string Name { get { return "ارسال ایمیل"; } } } }
- متد RunAt ثانیهای یکبار فراخوانی میشود (بنابراین بررسی now.Second را فراموش نکنید). زمان ارسالی به آن UTC است و اگر برای نمونه میخواهید بر اساس ساعت ایران کار کنید باید 3.5 ساعت به آن اضافه نمائید. این مساله برای سرورهایی که خارج از ایران قرار دارند مهم است. چون زمان محلی آنها برای تصمیم گیری در مورد زمان اجرای کارها مفید نیست.
در متد RunAt فرصت خواهید داشت تا منطق زمان اجرای وظیفهی جاری را مشخص کنید. برای نمونه در مثال فوق، این وظیفه هر دو دقیقه یکبار اجرا میشود. یا اگر خواستید اجرای آن فقط در سال 23 و 33 دقیقه هر روز باشد، تعریف آن به نحو ذیل خواهد بود:
public override bool RunAt(DateTime utcNow) { if (this.IsShuttingDown || this.Pause) return false; var now = utcNow.AddHours(3.5); return now.Hour == 23 && now.Minute == 33 && now.Second == 1; }
خاصیت Pause هر وظیفه را برنامه میتواند تغییر دهد. به این ترتیب در مورد توقف یا ادامهی یک وظیفه میتوان تصمیم گیری کرد. خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks، لیست وظایف تعریف شده را در اختیار شما قرار میدهد.
- در متد Run، منطق وظیفهی تعریف شده را باید مشخص کرد. برای مثال ارسال ایمیل یا تهیهی بک آپ.
- Name نیز نام وظیفهی جاری است که میتواند در گزارشات مفید باشد.
همین مقدار برای تعریف یک وظیفه کافی است.
نحوهی ثبت و راه اندازی وظایف تعریف شده
پس از اینکه چند وظیفه را تعریف کردیم، برای مدیریت بهتر آنها میتوان یک کلاس ثبت و معرفی کلی را مثلا به نام ScheduledTasksRegistry ایجاد کرد:
using System; using System.Net; namespace DNTScheduler.TestWebApplication.WebTasks { public static class ScheduledTasksRegistry { public static void Init() { ScheduledTasksCoordinator.Current.AddScheduledTasks( new SendEmailsTask(), new DoBackupTask()); ScheduledTasksCoordinator.Current.OnUnexpectedException = (exception, scheduledTask) => { //todo: log the exception. System.Diagnostics.Trace.WriteLine(scheduledTask.Name + ":" + exception.Message); }; ScheduledTasksCoordinator.Current.Start(); } public static void End() { ScheduledTasksCoordinator.Current.Dispose(); } public static void WakeUp(string pageUrl) { try { using (var client = new WebClient()) { client.Credentials = CredentialCache.DefaultNetworkCredentials; client.Headers.Add("User-Agent", "ScheduledTasks 1.0"); client.DownloadData(pageUrl); } } catch (Exception ex) { //todo: log ex System.Diagnostics.Trace.WriteLine(ex.Message); } } } }
- توسط متد ScheduledTasksCoordinator.Current.AddScheduledTasks، تنها کافی است کلاسهای وظایف مشتق شده از ScheduledTaskTemplate، معرفی شوند.
- به کمک متد ScheduledTasksCoordinator.Current.Start، کار Thread timer برنامه شروع میشود.
- اگر در حین اجرای متد Run، استثنایی رخ دهد، آنرا توسط یک Action delegate به نام ScheduledTasksCoordinator.Current.OnUnexpectedException میتوانید دریافت کنید. کتابخانهی DNT Scheduler برای اجرای وظایف، از یک ترد با سطح تقدم Below normal استفاده میکند تا در حین اجرای وظایف، برنامهی جاری با اخلال و کندی مواجه نشده و بتواند به درخواستهای رسیده پاسخ دهد. در این بین اگر استثنایی رخ دهد، میتواند کل پروسهی IIS را خاموش کند. به همین جهت این کتابخانه کار try/catch استثناهای متد Run را نیز انجام میدهد تا از این لحاظ مشکلی نباشد.
- متد ScheduledTasksCoordinator.Current.Dispose کار مدیر وظایف برنامه را خاتمه میدهد.
- از متد WakeUp تعریف شده میتوان برای بیدار کردن مجدد برنامه استفاده کرد.
استفاده از کلاس ScheduledTasksRegistry تعریف شده
پس از اینکه کلاس ScheduledTasksRegistry را تعریف کردیم، نیاز است آنرا به فایل استاندارد global.asax.cs برنامه به نحو ذیل معرفی کنیم:
using System; using System.Configuration; using DNTScheduler.TestWebApplication.WebTasks; namespace DNTScheduler.TestWebApplication { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { ScheduledTasksRegistry.Init(); } protected void Application_End() { ScheduledTasksRegistry.End(); //نکته مهم این روش نیاز به سرویس پینگ سایت برای زنده نگه داشتن آن است ScheduledTasksRegistry.WakeUp(ConfigurationManager.AppSettings["SiteRootUrl"]); } } }
- متد ScheduledTasksRegistry.End در پایان کار برنامه جهت پاکسازی منابع باید فراخوانی گردد.
همچنین در اینجا با فراخوانی ScheduledTasksRegistry.WakeUp، میتوانید برنامه را مجددا زنده کنید! IIS مجاز است یک سایت ASP.NET را پس از مثلا 20 دقیقه عدم فعالیت (فعالیت به معنای درخواستهای رسیده به سایت است و نه کارهای پس زمینه)، از حافظه خارج کند (این عدد در application pool برنامه قابل تنظیم است). در اینجا در فایل web.config برنامه میتوانید آدرس یکی از صفحات سایت را برای فراخوانی مجدد تعریف کنید:
<?xml version="1.0"?> <configuration> <appSettings> <add key="SiteRootUrl" value="http://localhost:10189/Default.aspx" /> </appSettings> </configuration>
گزارشگیری از وظایف تعریف شده
برای دسترسی به کلیه وظایف تعریف شده، از خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks استفاده نمائید:
var jobsList = ScheduledTasksCoordinator.Current.ScheduledTasks.Select(x => new { TaskName = x.Name, LastRunTime = x.LastRun, LastRunWasSuccessful = x.IsLastRunSuccessful, IsPaused = x.Pause, }).ToList();
تزریق وابستگیهای AutoMapper در لایه سرویس برنامه
public IList<AdvertismentViewModel> GetAdvertisementsByMe(int userId) { Mapper.CreateMap<Advertisement, AdvertismentViewModel>(); var adsList = _advertisements.Where(x => x.UserId == userId).Project().To<AdvertismentViewModel>().ToList(); return adsList; }
اعمال نشدن گروهبندی
public class CH_Rpt_Report1 : IPageHeader { public TermViewModel TermInfo { get; set; } public string StartDate { get; set; } public string EndDate { get; set; } public CH_Rpt_AllTeacherAbsents(TermViewModel termInfo, string startDate, string endDate) { TermInfo = termInfo; StartDate = startDate; EndDate = endDate; } public CH_Rpt_AllTeacherAbsents() { } public IPdfFont PdfRptFont { set; get; } public PdfGrid RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData) { var rootGrid = new PdfGrid(1); rootGrid.DefaultCell.BorderWidth = 0; rootGrid.WidthPercentage = 100; ... return grid; } public PdfGrid RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData) { var teacherCode = newGroupInfo.GetSafeStringValueOf<TeachersAbsentRow>(p => p.TeacherCode); var teacherFullName = newGroupInfo.GetSafeStringValueOf<TeachersAbsentRow>(p => p.TeacherFullName); var table = new PdfGrid(1) { WidthPercentage = 100, HorizontalAlignment = PdfWriter.RUN_DIRECTION_RTL, RunDirection = PdfWriter.RUN_DIRECTION_RTL }; table.AddSimpleRow((cellData, cellProperties) => { cellData.Value = (teacherFullName + " - " + teacherCode).FixWeakCharacters(); //cellData.Value = (teacherFullName); cellProperties.PdfFont = FontHelper.GetIPdfFont(FontHelper.GetFontPath(FarsiFonts.BYekan), new BaseColor(Color.Gray), 12); cellProperties.HorizontalAlignment = HorizontalAlignment.Right; }); return table.AddBorderToTable(borderColor: BaseColor.LIGHT_GRAY, spacingBefore: 5f); } }
public class Rpt_AllTeacherAbsents : IReportBase { #region IReportBase public string ReportFileName { get; set; } #endregion IReportBase #region Properties public bool ShowTeacherInSeperatePage { get; set; } public List<TeachersAbsentRow> DataSource { get; set; } public string StartDate { get; set; } public string EndDate { get; set; } public bool ShowFooter { get; set; } public TermViewModel Term { get; set; } #endregion Properties #region Constructors public Rpt_AllTeacherAbsents() { } public Rpt_AllTeacherAbsents(List<TeachersAbsentRow> dataSource, TermViewModel term, string startDate = "", string endDate = "", bool showFooter = true, bool showTeacherInSeperatePage = false) { ShowFooter = showFooter; DataSource = dataSource; Term = term; StartDate = startDate; EndDate = endDate; ShowTeacherInSeperatePage = showTeacherInSeperatePage; } #endregion Constructors public IPdfReportData Create() { return new PdfReport() .DocumentPreferences(doc => { doc.RunDirection(PdfRunDirection.RightToLeft); doc.Orientation(PageOrientation.Landscape); doc.PageSize(PdfPageSize.A4); doc.DocumentMetadata(new DocumentMetadata { ... }); }) .DefaultFonts(fonts => { fonts.Size(8); fonts.Path(FontHelper.GetFontPath(FarsiFonts.BYekan), //fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\BYekan.ttf", Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf"); }) .PagesFooter(footer => { #region Footer if (ShowFooter) { footer.XHtmlFooter(rptFooter => { IPdfFont ipf = footer.PdfFont; ipf.Size = 9; rptFooter.PageFooterProperties(new XFooterBasicProperties { RunDirection = PdfRunDirection.RightToLeft, ShowBorder = true, PdfFont = ipf, TotalPagesCountTemplateHeight = 10, TotalPagesCountTemplateWidth = 50, SpacingBeforeTable = 25f, InlineCss = "border:0px solid;border-top:1px solid black;" }); //RazorMachine rm = new RazorMachine(); rptFooter.AddPageFooter(pageFooter => { return HeaderAndFooterUtility.InitCommonFooter(Term, pageFooter); }); }); } #endregion }) .PagesHeader(header => { header.CustomHeader(new CH_Rpt_AllTeacherAbsents(Term, StartDate, EndDate) { PdfRptFont = header.PdfFont }); }) .MainTableTemplate(template => { template.CustomTemplate(new GrayTemplate(false)); }) .MainTablePreferences(table => { #region table.ShowHeaderRow(true); table.SpacingAfter(10f); table.ColumnsWidthsType(TableColumnWidthType.Absolute); table.NumberOfDataRowsPerPage(0); table.GroupsPreferences(new GroupsPreferences { GroupType = GroupType.HideGroupingColumns, SpacingBeforeAllGroupsSummary = 1f, SpacingAfterAllGroupsSummary = 5, NewGroupAvailableSpacingThreshold = 30, RepeatHeaderRowPerGroup = true, ShowOneGroupPerPage = ShowTeacherInSeperatePage, }); #endregion }) .MainTableDataSource(dataSource => { dataSource.StronglyTypedList<TeachersAbsentRow>(DataSource); }) .MainTableSummarySettings(summarySettings => { //summarySettings.OverallSummarySettings("جمع ساعات تدریس : "); // summarySettings.PreviousPageSummarySettings("Previous Page Summary"); // summarySettings.PageSummarySettings("Page Summary"); }) .MainTableColumns(columns => { #region Columns columns.AddColumn(column => { column.PropertyName("rowNo"); column.IsRowNumber(true); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(0); column.Width(40); column.HeaderCell("#", horizontalAlignment: HorizontalAlignment.Center); }); columns.AddColumn(column => { column.PropertyName<TeachersAbsentRow>(x => x.TeacherCode); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(100); column.HeaderCell("کد استاد", horizontalAlignment: HorizontalAlignment.Right); column.Group(true, (val1, val2) => { return val1.ToString() == val2.ToString(); }); }); columns.AddColumn(column => { column.PropertyName<TeachersAbsentRow>(x => x.LessonCode); column.CellsHorizontalAlignment(HorizontalAlignment.Left); column.IsVisible(true); column.Order(2); column.Width(100); column.HeaderCell("کد درس", horizontalAlignment: HorizontalAlignment.Center); }); columns.AddColumn(column => { column.PropertyName<TeachersAbsentRow>(x => x.LessonName); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(200); column.HeaderCell("نام درس", horizontalAlignment: HorizontalAlignment.Center); }); columns.AddColumn(column => { column.PropertyName<TeachersAbsentRow>(x => x.Date); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(90); column.HeaderCell("تاریخ", horizontalAlignment: HorizontalAlignment.Center); }); columns.AddColumn(column => { column.PropertyName<TeachersAbsentRow>(x => x.StartTimeToEndTime); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(120); column.HeaderCell("ساعت", horizontalAlignment: HorizontalAlignment.Center); }); columns.AddColumn(column => { column.PropertyName<TeachersAbsentRow>(x => x.TrendCode); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(90); column.HeaderCell("کد رشته", horizontalAlignment: HorizontalAlignment.Center); }); columns.AddColumn(column => { column.PropertyName<TeachersAbsentRow>(x => x.TrendName); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(180); column.HeaderCell("عنوان رشته", horizontalAlignment: HorizontalAlignment.Center); }); columns.AddColumn(column => { column.PropertyName<TeachersAbsentRow>(x => x.PlaceFullName); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(130); column.HeaderCell("مکان برگزاری", horizontalAlignment: HorizontalAlignment.Center); }); #endregion }) .MainTableEvents(events => { #region Events #region Alternate Row Colors events.CellCreated(args => { }); #endregion events.DataSourceIsEmpty(message: "داده ای برای مشاهده وجود ندارد."); events.CellCreated(args => { if (args.RowType == RowType.DataTableRow) { } }); #endregion }) .Export(export => { }) .Generate(data => data.AsPdfFile(ReportFileName)); } }
public class TeachersAbsentRow { public TeachersAbsentRow() { } public int Id { get; set; } public int ProgramId { get; set; } public string RowNumber { get; set; } public bool IsSelected { get; set; } public int TeacherId { get; set; } public string TeacherCode { get; set; } public string TeacherFullName { get; set; } public string TeacherFirstName { get; set; } public string TeacherLastName { get; set; } public string GroupNumber { get; set; } public string GroupDescription { get; set; } public string LessonId { get; set; } public string LessonCode { get; set; } public string LessonName { get; set; } public int LessonPresentationTerm { get; set; } public string TrendId { get; set; } public string TrendCode { get; set; } public string TrendName { get; set; } public string Date { get; set; } public string StartTime { get; set; } public string EndTime { get; set; } public string StartTimeToEndTime { get; set; } public string DayNumber { get; set; } public string DayTitle { get; set; } public int BuildingId { get; set; } public int PlaceId { get; set; } public string PlaceName { get; set; } public string BuildingName { get; set; } public string PlaceFullName { get; set; } }
استفاده از این صفت هم بسیار ساده است:
private void A ( [CallerMemberName] string callerName = "") { Console.WriteLine("Caller is " + callerName); } private static void B() { // let's call A A(); }
ولی یک استفادهی بسیار کاربردی از این صفت، در پیاده سازی رابط INotifyPropertyChanged میباشد.
معمولا هنگام پیاده سازی INotifyPropertyChanged کدی شبیه به این را مینویسیم:
public class PersonViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } private string name; public string Name { get { return name; } set { this.name = value; OnPropertyChanged("Name"); } } }
یعنی در Setter معمولا نام ویژگی ای را که تغییر کرده است، به متد OnPropertyChanged میفرستیم تا اطلاع رسانیهای لازم انجام پذیرد. تا اینجای کار همه چیز خوب و آرام است. اما به محضی که کد شما کمی طولانی شود و شما به دلایلی نیاز به Refactor کردن کد و احیانا تغییر نام ویژگیها را پیدا کنید، آن موقع مسائل جدیدی بروز پیدا میکند.
برای مثال فرض کنید پس از نوشتن کلاس PersonViewModel تصمیم میگیرد نام ویژگی Name را به FirstName تغییر دهید؛ چرا که میخواهید اجزای نام یک شخص را به صورت مجزا نگهداری و پردازش کنید. پس احتمالا با زدن کلید F2 روی فیلد name آن را به firstName و ویژگی Name را به FirstName تغییر نام میدهید. همانند کد زیر:
private string firstName; public string FirstName { get { return firstName; } set { this.firstName = value; OnPropertyChanged("Name"); } }
برنامه را کامپایل کرده و در کمال تعجب میبینید که بخشی از برنامه درست رفتار نمیکند و تغییراتی که در نام کوچک شخص توسط کاربر ایجاد میشود به درستی بروزرسانی نمیشوند. علت ساده است: ما کد را به صورت اتوماتیک Refactor کرده ایم و گزینهی Include String را در حین Refactor، در حالت پیشفرض غیرفعال رها کردهایم. پس جای تعجبی ندارد که در هر جای کد که رشتهای به نام "Name" با ماهیت نام شخص داشته ایم، دست نخورده باقی مانده است. در واقع در کد تغییر یافته، هنگام تغییر FirstName، ما به سیستم گزارش میکنیم که ویژگی Name (که اصلا وجود ندارد) تغییر یافته است و این یعنی خطا.
حال احتمال بروز این خطا را در ViewModel هایی با دهها ویژگی و ترکیبهای مختلف در نظر بگیرید. پس کاملا محتمل است و برای خیلی از دوستان این اتفاق رخ داده است.
و اما راه حل چیست؟ به کارگیری صفت CallerMemberName
بهتر است که یک کلاس انتزاعی برای تمام ViewModelهای خود داشته باشیم و پیاده سازی جدید INPC را در درون آن قرار دهیم تا براحتی VMهای ما از آن مشتق شوند:
public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = "") { OnPropertyChangedExplicit(propertyName); } protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> projection) { var memberExpression = (MemberExpression)projection.Body; OnPropertyChangedExplicit(memberExpression.Member.Name); } void OnPropertyChangedExplicit(string propertyName) { this.CheckPropertyName(propertyName); PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { var e = new PropertyChangedEventArgs(propertyName); handler(this, e); } } #region Check property name [Conditional("DEBUG")] [DebuggerStepThrough] public void CheckPropertyName(string propertyName) { if (TypeDescriptor.GetProperties(this)[propertyName] == null) throw new Exception(String.Format("Could not find property \"{0}\"", propertyName)); } #endregion // Check property name }
در این کلاس، ما پارامتر propertyName را از متد OnPropertyChanged، توسط صفت CallerMemberName حاشیه نویسی کردهایم. این کار باعث میشود در Setterهای ویژگیها، به راحتی بدون نوشتن نام ویژگی، عملیات اطلاع رسانی تغییرات را انجام دهیم. بدین صورت که کافیست متد OnPropertyChanged بدون هیچ آرگومانی در Setter فراخوانی شود و صفت CallerMemberName به صورت اتوماتیک نام ویژگی ای که فراخوانی از درون آن انجام شده است را درون پارامتر propertyName قرار میدهد.
پس کلاس PersonViewModel را به صورت زیر میتوانیم اصلاح و تکمیل کنیم:
public class PersonViewModel : ViewModelBase { private string firstName; public string FirstName { get { return firstName; } set { this.firstName = value; OnPropertyChanged(); OnPropertyChanged(() => this.FullName); } } private string lastName; public string LastName { get { return lastName; } set { this.lastName = value; OnPropertyChanged(); OnPropertyChanged(() => this.FullName); } } public string FullName { get { return string.Format("{0} {1}", FirstName, LastName); } } }
کلاس ViewModelBase یک پیاده سازی دیگر از OnPropetyChanged هم دارد که به شما اجازه میدهد با استفاده دستورات لامبدا، OnPropertyChanged را برای هر یک از اعضای دلخواه کلاس نیز فراخوانی کنید. همانطور که در مثال فوق میبینید، تغییرات نام خانوادگی در نام کامل شخص نیز اثرگذار است. در نتیجه به وسیلهی یک Func به راحتی بیان میکنیم که FullName هم تغییر کرده است و اطلاع رسانی برای آن نیز باید صورت پذیرد.
برای استفاده از صفت CallerMemberName باید دات نت هدف خود را 4.5 یا 4.6 قرار دهید.
ارجاع:
Raise INPC witout string name
UserDialogs.Init(this); // Before Forms.Init
containerBuilder.RegisterInstance(UserDialogs.Instance);
public IUserDialogs UserDialogs { get; set; } public async Task Login() { if (string.IsNullOrWhiteSpace(UserName) || string.IsNullOrWhiteSpace(Password)) await UserDialogs.AlertAsync(message: "Please provide UserName and Password!", title: ")-:", okText: "Ok!"); }
using (UserDialogs.Loading("Logging in...", maskType: MaskType.Black)) { // Login implementation ... await Task.Delay(TimeSpan.FromSeconds(3)); }
await NavigationService.NavigateAsync("/Login", animated: false);
await NavigationService.NavigateAsync("/Nav/HelloWorld");
3- ممکن است بخواهید هنگام رفتن از صفحهای به صفحه دیگر، پارامتر نیز ارسال کنید. اگر برای مثال صفحه اول لیست محصولات را نمایش میدهد و با زدن روی هر محصول قرار است به صفحهای برویم که جزئیات آن محصول را ببینیم، بهتر است Id آن محصول به صورت پارامتر به صفحه دوم ارسال شود. برای این کار داریم:
await NavigationService.NavigateAsync("ProductDetail", new NavigationParameters { { "productId", productId } });
حال سؤال این است که در صفحه جزئیات یک محصول، چگونه productId را بگیریم؟ فرض کنید دو صفحه ProductsList و ProductDetail را داریم. هر صفحه دارای View و View Model است. در ViewModel مربوط به ProductDetail، یعنی ProductDetailViewModel که از BitViewModelBase ارث بری کردهاست، میتوانیم متد OnNavigatedToAsync را override کنیم. در آنجا به پارامترهای ارسال شده دسترسی داریم:
public async override Task OnNavigatedToAsync(INavigationParameters parameters) { await base.OnNavigatedToAsync(parameters); Guid productId = parameters.GetValue<Guid>("productId"); }
هر ViewModel علاوه بر OnNavigatedTo می تواند دارای OnNavigatedFrom هم باشد که زمانیکه داریم از صفحه مربوطه خارج میشویم، فراخوانی میشود.
4- برای نمایش صفحه به صورت Popup کافی است بجای اینکه View ما یک Content Page باشد، یک PopupPage باشد (برای درک بهتر، فایل IntroView.xaml را در فولدر Views باز کنید).
حتی میتوانید Animation مربوط به باز شدن پاپ آپ را هم کاملا Customize کنید. مثلا زمان باز شدن، از سمت راست صفحه وارد شود و زمان خارج شدن، Fade out شود. باز کردن Popup در Navigation Page معنی نمیدهد، پس با Nav/ در اینجا کاری نداریم. در مثال ما، بعد از لاگین میخواهیم یک صفحه Intro شامل هشدارها و راهنماییهای اولیه را در قالب Popup به کاربر نمایش دهیم. Popupها میتوانند همچون Content Pageها، دارای View Model باشند و مواردی چون OnNavigatedTo، ارسال پارامتر و هر آنچه که گفته شد، در مورد آنها نیز صدق میکند.
5- برای Master/Detail کافی است بجای Nav/HelloWorld/ از MasterDetail/Nav/HelloWorld/ استفاده کنید. این عمل باعث میشود HelloWorld در داخل Navigation Page و Navigation Page داخل Master Detail باز شود. از این سادهتر امکان ندارد!
برای تغییر UI مربوط به Master که از سمت چپ باز میشود، فایل XamAppMasterDetailView.xaml را تغییر دهید.
در قسمت بعدی به جزئیات Binding خواهیم پرداخت.
EF Code First #14
ردیابی تغییرات در EF Code first
EF از DbContext برای ذخیره اطلاعات مرتبط با تغییرات موجودیتهای تحت کنترل خود کمک میگیرد. این نوع اطلاعات توسط Change Tracker API جهت بررسی وضعیت فعلی یک شیء، مقادیر اصلی و مقادیر تغییر کرده آن در دسترس هستند. همچنین در اینجا امکان بارگذاری مجدد اطلاعات موجودیتها از بانک اطلاعاتی جهت اطمینان از به روز بودن آنها تدارک دیده شده است. سادهترین روش دستیابی به این اطلاعات، استفاده از متد context.Entry میباشد که یک وهله از موجودیتی خاص را دریافت کرده و سپس به کمک خاصیت State خروجی آن، وضعیتهایی مانند Unchanged یا Modified را میتوان به دست آورد. علاوه بر آن خروجی متد context.Entry، دارای خواصی مانند CurrentValues و OriginalValues نیز میباشد. OriginalValues شامل مقادیر خواص موجودیت درست در لحظه اولین بارگذاری در DbContext برنامه است. CurrentValues مقادیر جاری و تغییر یافته موجودیت را باز میگرداند. به علاوه این خروجی امکان فراخوانی متد GetDatabaseValues را جهت بدست آوردن مقادیر جدید ذخیره شده در بانک اطلاعاتی نیز ارائه میدهد. ممکن است در این بین، خارج از Context جاری، اطلاعات بانک اطلاعاتی توسط کاربر دیگری تغییر کرده باشد. به کمک GetDatabaseValues میتوان به این نوع اطلاعات نیز دست یافت.
حداقل چهار کاربرد عملی جالب را از اطلاعات موجود در Change Tracker API میتوان مثال زد که در ادامه به بررسی آنها خواهیم پرداخت.
کلاسهای مدل مثال جاری
در اینجا یک رابطه many-to-one بین جدول هزینههای اقلام خریداری شده یک شخص و جدول فروشندگان تعریف شده است:
using System;
namespace EF_Sample09.DomainClasses
{
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedOn { set; get; }
public string CreatedBy { set; get; }
public DateTime ModifiedOn { set; get; }
public string ModifiedBy { set; get; }
}
}
using System;
namespace EF_Sample09.DomainClasses
{
public class Bill : BaseEntity
{
public decimal Amount { set; get; }
public string Description { get; set; }
public virtual Payee Payee { get; set; }
}
}
using System.Collections.Generic;
namespace EF_Sample09.DomainClasses
{
public class Payee : BaseEntity
{
public string Name { get; set; }
public virtual ICollection<Bill> Bills { set; get; }
}
}
به علاوه همانطور که ملاحظه میکنید، این کلاسها از یک abstract class به نام BaseEntity مشتق شدهاند. هدف از این کلاس پایه تنها تامین یک سری خواص تکراری در کلاسهای برنامه است و هدف از آن، مباحث ارث بری مانند TPH، TPT و TPC نیست.
به همین جهت برای اینکه این کلاس پایه تبدیل به یک جدول مجزا و یا سبب یکی شدن تمام کلاسها در یک جدول نشود، تنها کافی است آنرا به عنوان DbSet معرفی نکنیم و یا میتوان از متد Ignore نیز استفاده کرد:
using System.Data.Entity;
using EF_Sample09.DomainClasses;
namespace EF_Sample09.DataLayer.Context
{
public class Sample09Context : MyDbContextBase
{
public DbSet<Bill> Bills { set; get; }
public DbSet<Payee> Payees { set; get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Ignore<BaseEntity>();
base.OnModelCreating(modelBuilder);
}
}
}
الف) به روز رسانی اطلاعات Context در صورتیکه از متد context.Database.ExecuteSqlCommand مستقیما استفاده شود
در قسمت قبل با متد context.Database.ExecuteSqlCommand برای اجرای مستقیم عبارات SQL بر روی بانک اطلاعاتی آشنا شدیم. اگر این متد در نیمه کار یک Context فراخوانی شود، به معنای کنار گذاشتن Change Tracker API میباشد؛ زیرا اکنون در سمت بانک اطلاعاتی اتفاقاتی رخ دادهاند که هنوز در Context جاری کلاینت منعکس نشدهاند:
using System;
using System.Data.Entity;
using EF_Sample09.DataLayer.Context;
using EF_Sample09.DomainClasses;
namespace EF_Sample09
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample09Context, Configuration>());
using (var db = new Sample09Context())
{
var payee = new Payee { Name = "فروشگاه سر کوچه" };
var bill = new Bill { Amount = 4900, Description = "یک سطل ماست", Payee = payee };
db.Bills.Add(bill);
db.SaveChanges();
}
using (var db = new Sample09Context())
{
var bill1 = db.Bills.Find(1);
bill1.Description = "ماست";
db.Database.ExecuteSqlCommand("Update Bills set Description=N'سطل ماست' where id=1");
Console.WriteLine(bill1.Description);
db.Entry(bill1).Reload(); //Refreshing an Entity from the Database
Console.WriteLine(bill1.Description);
db.SaveChanges();
}
}
}
}
در این مثال ابتدا دو رکورد به بانک اطلاعاتی اضافه میشوند. سپس توسط متد db.Bills.Find، اولین رکورد جدول Bills بازگشت داده میشود. در ادامه، خاصیت توضیحات آن به روز شده و سپس با استفاده از متد db.Database.ExecuteSqlCommand نیز بار دیگر خاصیت توضیحات اولین رکورد به روز خواهد شد.
اکنون اگر مقدار bill1.Description را بررسی کنیم، هنوز دارای مقدار پیش از فراخوانی db.Database.ExecuteSqlCommand میباشد، زیرا تغییرات سمت بانک اطلاعاتی هنوز به Context مورد استفاده منعکس نشده است.
در اینجا برای هماهنگی کلاینت با بانک اطلاعاتی، کافی است متد Reload را بر روی موجودیت مورد نظر فراخوانی کنیم.
ب) یکسان سازی ی و ک اطلاعات رشتهای دریافتی پیش از ذخیره سازی در بانک اطلاعاتی
یکی از الزامات برنامههای فارسی، یکسان سازی ی و ک دریافتی از کاربر است. برای این منظور باید پیش از فراخوانی متد SaveChanges نهایی، مقادیر رشتهای کلیه موجودیتها را یافته و به روز رسانی کرد:
using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Reflection;
using EF_Sample09.DataLayer.Toolkit;
using EF_Sample09.DomainClasses;
namespace EF_Sample09.DataLayer.Context
{
public class MyDbContextBase : DbContext
{
public void RejectChanges()
{
foreach (var entry in this.ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Modified:
entry.State = EntityState.Unchanged;
break;
case EntityState.Added:
entry.State = EntityState.Detached;
break;
}
}
}
public override int SaveChanges()
{
applyCorrectYeKe();
auditFields();
return base.SaveChanges();
}
private void applyCorrectYeKe()
{
//پیدا کردن موجودیتهای تغییر کرده
var changedEntities = this.ChangeTracker
.Entries()
.Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);
foreach (var item in changedEntities)
{
if (item.Entity == null) continue;
//یافتن خواص قابل تنظیم و رشتهای این موجودیتها
var propertyInfos = item.Entity.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance
).Where(p => p.CanRead && p.CanWrite && p.PropertyType == typeof(string));
var pr = new PropertyReflector();
//اعمال یکپارچگی نهایی
foreach (var propertyInfo in propertyInfos)
{
var propName = propertyInfo.Name;
var val = pr.GetValue(item.Entity, propName);
if (val != null)
{
var newVal = val.ToString().Replace("ی", "ی").Replace("ک", "ک");
if (newVal == val.ToString()) continue;
pr.SetValue(item.Entity, propName, newVal);
}
}
}
}
private void auditFields()
{
// var auditUser = User.Identity.Name; // in web apps
var auditDate = DateTime.Now;
foreach (var entry in this.ChangeTracker.Entries<BaseEntity>())
{
// Note: You must add a reference to assembly : System.Data.Entity
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedOn = auditDate;
entry.Entity.ModifiedOn = auditDate;
entry.Entity.CreatedBy = "auditUser";
entry.Entity.ModifiedBy = "auditUser";
break;
case EntityState.Modified:
entry.Entity.ModifiedOn = auditDate;
entry.Entity.ModifiedBy = "auditUser";
break;
}
}
}
}
}
اگر به کلاس Context مثال جاری که در ابتدای بحث معرفی شد دقت کرده باشید به این نحو تعریف شده است (بجای DbContext از MyDbContextBase مشتق شده):
public class Sample09Context : MyDbContextBase
علت هم این است که یک سری کد تکراری را که میتوان در تمام Contextها قرار داد، بهتر است در یک کلاس پایه تعریف کرده و سپس از آن ارث بری کرد.
تعاریف کامل کلاس MyDbContextBase را در کدهای فوق ملاحظه میکنید.
در اینجا کار با تحریف متد SaveChanges شروع میشود. سپس در متد applyCorrectYeKe کلیه موجودیتهای تحت نظر ChangeTracker که تغییر کرده باشند یا به آن اضافه شده باشند، یافت شده و سپس خواص رشتهای آنها جهت یکسانی سازی ی و ک، بررسی میشوند.
ج) سادهتر سازی به روز رسانی فیلدهای بازبینی یک رکورد مانند DateCreated، DateLastUpdated و امثال آن بر اساس وضعیت جاری یک موجودیت
در کلاس MyDbContextBase فوق، کار متد auditFields، مقدار دهی خودکار خواص تکراری تاریخ ایجاد، تاریخ به روز رسانی، شخص ایجاد کننده و شخص تغییر دهنده یک رکورد است. به کمک ChangeTracker میتوان به موجودیتهایی از نوع کلاس پایه BaseEntity دست یافت. در اینجا اگر entry.State آنها مساوی EntityState.Added بود، هر چهار خاصیت یاد شده به روز میشوند. اگر حالت موجودیت جاری، EntityState.Modified بود، تنها خواص مرتبط با تغییرات رکورد به روز خواهند شد.
به این ترتیب دیگر نیازی نیست تا در حین ثبت یا ویرایش اطلاعات برنامه نگران این چهار خاصیت باشیم؛ زیرا به صورت خودکار مقدار دهی خواهند شد.
د) پیاده سازی قابلیت لغو تغییرات در برنامه
علاوه بر اینها در کلاس MyDbContextBase، متد RejectChanges نیز تعریف شده است تا بتوان در صورت نیاز، حالت موجودیتهای تغییر کرده یا اضافه شده را به حالت پیش از عملیات، بازگرداند.