protected override IQueryable<BlogModel> BuildReadQuery(FilteredPagedQueryModel model) { return EntitySet.AsNoTracking().Select(b => new BlogModel {Id = b.Id, RowVersion = b.RowVersion, Url = b.Url, Title = b.Title}); }
در مطلب جاری، فرض بر این است که توابع جاوا اسکریپتی، سراسری هستند و قرار است در تمام کامپوننتهای برنامه قابل دسترسی باشند. به همین جهت ارجاع مستقیمی از فایل js. آنها را در فایل index.html و یا Host_، قرار میدهیم. اما اگر تنها یک کامپوننت، نیاز به اسکریپت خاصی را داشته باشد و نه تمام کامپوننتهای دیگر، چطور؟
در این حالت Blazor از مفهومی به نام JavaScript Isolation پشتیبانی میکند. برای توضیح آن، فایل جدید Client\wwwroot\MyMdl.Js را به پروژه اضافه کرده و سپس به صورت زیر تکمیل میکنیم:
export function showPrompt(message) { return prompt(message, "Type name"); } export function showAlert(message) { return prompt(message, "Hello"); }
- برخلاف قبل، مدخل جدیدی را از این فایل، به فایلهای index.html و یا Host_ اضافه نمیکنیم. چون میخواهیم فقط کامپوننتی که به آن نیاز دارد، آنرا بارگذاری کند.
سپس کامپوننت جدید Client\Pages\JsIsolation.razor را به صورت زیر تکمیل خواهیم کرد:
@page "/js-isolation" @inject IJSRuntime jSRuntime <button class="btn btn-primary" @onclick="Prompt">Prompt</button> <button class="btn btn-primary" @onclick="ShowAlert">Alert</button> @code { private IJSObjectReference module; protected override async Task OnAfterRenderAsync(bool firstRender) { if(firstRender) { module = await jSRuntime.InvokeAsync<IJSObjectReference>("import", "./MyMdl.Js"); } } private async Task Prompt() { var result = await module.InvokeAsync<string>("showPrompt", "What's your name?"); } private async Task ShowAlert() { await module.InvokeVoidAsync("showAlert", "Hello!"); } }
- اکنون که module یا IJSObjectReference را در اختیار داریم، میتوان با استفاده از متدهای InvokeAsync و یا InvokeVoidAsync، با متدهای موجود در آن کار کرد.
- پس از خداحافظی با شرکتی که در آن کار میکردی، شخصی با پوزخند به شما میگوید که «میدونستی در برنامهی حق و دستمزد شما، بچههای ادمین شبکه، دیتابیس برنامه رو مستقیما دستکاری میکردند و تعداد ساعات کاری بیشتری رو وارد میکردند»؟!
- مسئول فروشی/مسئول پذیرشی که یاد گرفته چطور به صورت مستقیم به بانک اطلاعاتی دسترسی پیدا کند و آمار فروش/پذیرش روز خودش را در بانک اطلاعاتی، با دستکاری مستقیم و خارج از برنامه، کمتر از مقدار واقعی نمایش دهد.
- باز هم مدیر سیستمی/شبکهای که دسترسی مستقیم به بانک اطلاعاتی دارد، در ساعاتی مشخص، کلمهی عبور هش شدهی خودش را مستقیما، بجای کلمهی عبور ادمین برنامه در بانک اطلاعاتی وارد کرده و پس از آن ...
این موارد متاسفانه واقعی هستند! اکنون سؤال اینجا است که آیا برنامهی شما قادر است تشخیص دهد رکوردهایی که هم اکنون در بانک اطلاعاتی ثبت شدهاند، واقعا توسط برنامه و تمام سطوح دسترسی که برای آن طراحی کردهاید، به این شکل درآمدهاند، یا اینکه توسط اشخاصی به صورت مستقیم و با دور زدن کامل برنامه، از طریق management studioهای مختلف، در سیستم وارد و دستکاری شدهاند؟! در ادامه راه حلی را برای بررسی این مشکل مهم، مرور خواهیم کرد.
چگونه تغییرات رکوردها را در بانکهای اطلاعاتی ردیابی کنیم؟
روش متداولی که برای بررسی تغییرات رکوردها مورد استفاده قرار میگیرد، هش کردن تمام اطلاعات یک ردیف از جدول است و سپس مقایسهی این هشها با هم. علت استفادهی از الگوریتمهای هش نیز، حداقل به دو علت است:
- با تغییر حتی یک بیت از اطلاعات، مقدار هش تولید شده تغییر میکند.
- طول نهایی مقدار هش شدهی اطلاعاتی حجیم، بسیار کم است و به راحتی توسط بانکهای اطلاعاتی، قابل مدیریت و جستجو است.
اگر از SQL Server استفاده میکنید، یک چنین قابلیتی را به صورت توکار به همراه دارد:
SELECT [Id], (SELECT top 1 * FROM [AppUsers] FOR XML auto), HASHBYTES ('SHA2_256', (SELECT top 1 * FROM [AppUsers] FOR XML auto)) AS [hash] -- varbinary(n), since 2012 FROM [AppUsers]
کاری که این کوئری انجام میدهد شامل دو مرحله است:
الف) کوئری "SELECT top 1 * FROM [AppUsers] FOR XML auto" کاری شبیه به serialization را انجام میدهد. همانطور که مشاهده میکنید، نام و مقادیر تمام فیلدهای یک ردیف را به صورت یک خروجی XML در میآورد. بنابراین دیگر نیازی نیست تا کار تبدیل مقادیر تمام ستونهای یک ردیف را به عبارتی قابل هش، به صورت دستی انجام دهیم؛ رشتهی XML ای آن هم اکنون آمادهاست.
ب) متد HASHBYTES، این خروجی serialized را با الگوریتم SHA2_256، هش میکند. الگوریتمهای SHA2_256 و همچنین SHA2_512، از سال 2012 به بعد به SQL Server اضافه شدهاند.
اکنون اگر این هش را به نحوی ذخیره کنیم (برنامه باید این هش را ذخیره و یا به روز رسانی کند) و سپس شخصی به صورت مستقیم ردیف فوق را در بانک اطلاعاتی تغییر دهد، هش جدید این ردیف، با هش قبلی ذخیره شدهی توسط برنامه، یکی نخواهد بود که بیانگر دستکاری مستقیم این ردیف، خارج از برنامه و با دور زدن کامل تمام سطوح دسترسی آن است.
چگونه تغییرات رکوردها را در بانکهای اطلاعاتی، توسط EF Core ردیابی کنیم؟
مزیت روش فوق، توکار بودن آن است که کارآیی فوق العادهای را نیز به همراه دارد. اما چون در ادامه قصد داریم از یک ORM استفاده کنیم و ORMها نیز قرار است توانایی کار کردن با انواع و اقسام بانکهای اطلاعاتی را داشته باشند، دو مرحلهی serialization و هش کردن را در کدهای برنامه و با مدیریت EF Core، مستقل از بانک اطلاعاتی خاصی، انجام خواهیم داد.
معرفی موجودیتهای برنامه
در مثالی که بررسی خواهیم کرد، دو موجودیت Blog و Post تعریف شدهاند:
using System.Collections.Generic; namespace EFCoreRowIntegrity { public interface IAuditableEntity { string Hash { set; get; } } public static class AuditableShadowProperties { public static readonly string CreatedDateTime = nameof(CreatedDateTime); public static readonly string ModifiedDateTime = nameof(ModifiedDateTime); } public class Blog : IAuditableEntity { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } public string Hash { get; set; } } public class Post : IAuditableEntity { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } public string Hash { get; set; } } }
- به علاوه جهت تکمیل بحث، دو خاصیت سایهای نیز تعریف شدهاند تا بررسی کنیم که آیا هش اینها نیز درست محاسبه میشود یا خیر.
- علت اینکه خاصیت Hash، سایهای تعریف نشد، سهولت دسترسی و بالا بردن کارآیی آن بود.
معرفی ظرفی برای نگهداری نام خواص و مقادیر متناظر با یک موجودیت
در ادامه دو کلاس AuditEntry و AuditProperty را مشاهده میکنید:
using System.Collections.Generic; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace EFCoreRowIntegrity { public class AuditEntry { public EntityEntry EntityEntry { set; get; } public IList<AuditProperty> AuditProperties { set; get; } = new List<AuditProperty>(); public AuditEntry() { } public AuditEntry(EntityEntry entry) { EntityEntry = entry; } } public class AuditProperty { public string Name { set; get; } public object Value { set; get; } public bool IsTemporary { set; get; } public PropertyEntry PropertyEntry { set; get; } public AuditProperty() { } public AuditProperty(string name, object value, bool isTemporary, PropertyEntry property) { Name = name; Value = value; IsTemporary = isTemporary; PropertyEntry = property; } } }
معرفی روشی برای هش کردن مقادیر یک شیء
زمانیکه توسط سیستم Tracking، در حال کاربر بر روی موجودیتهای اضافه شده و یا ویرایش شده هستیم، میخواهیم فیلد هش آنها را نیز به صورت خودکار ویرایش و مقدار دهی کنیم. کلاس زیر، منطق ارائه دهندهی این مقدار هش را بیان میکند:
using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Newtonsoft.Json; namespace EFCoreRowIntegrity { public static class HashingExtensions { public static string GenerateObjectHash(this object @object) { if (@object == null) { return string.Empty; } var jsonData = JsonConvert.SerializeObject(@object, Formatting.Indented); using (var hashAlgorithm = new SHA256CryptoServiceProvider()) { var byteValue = Encoding.UTF8.GetBytes(jsonData); var byteHash = hashAlgorithm.ComputeHash(byteValue); return Convert.ToBase64String(byteHash); } } public static string GenerateEntityEntryHash(this EntityEntry entry, string propertyToIgnore) { var auditEntry = new Dictionary<string, object>(); foreach (var property in entry.Properties) { var propertyName = property.Metadata.Name; if (propertyName == propertyToIgnore) { continue; } auditEntry[propertyName] = property.CurrentValue; } return auditEntry.GenerateObjectHash(); } public static string GenerateEntityHash<TEntity>(this DbContext context, TEntity entity, string propertyToIgnore) { return context.Entry(entity).GenerateEntityEntryHash(propertyToIgnore); } } }
- نکتهی مهم: ما نمیخواهیم تمام خواص یک موجودیت را هش کنیم. برای مثال اگر موجودیتی دارای چندین رابطه با جداول دیگری بود، ما مقادیر اینها را هش نمیکنیم (چون رکوردهای متناظر با آنها در جداول خودشان میتوانند دارای فیلد هش مخصوصی باشند). بنابراین یک Dictionary را از خواص و مقادیر متناظر با آنها تشکیل داده و این Dictionary را تبدیل به JSON میکنیم.
- همچنین در این بین، مقدار خود فیلد Hash یک شیء نیز نباید در هش محاسبه شده، حضور داشته باشد. به همین جهت پارامتر propertyToIgnore را مشاهده میکنید.
معرفی Context برنامه که کار هش کردن خودکار موجودیتها را انجام میدهد
اکنون نوبت استفاده از تنظیمات انجام شدهی تا این مرحلهاست:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.Extensions.Logging; namespace EFCoreRowIntegrity { public class BloggingContext : DbContext { public BloggingContext() { } public BloggingContext(DbContextOptions options) : base(options) { } public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.EnableSensitiveDataLogging(); var path = Path.Combine(Directory.GetCurrentDirectory(), "app_data", "EFCore.RowIntegrity.mdf"); optionsBuilder.UseSqlServer($"Server=(localdb)\\mssqllocaldb;Database=EFCore.RowIntegrity;AttachDbFilename={path};Trusted_Connection=True;"); optionsBuilder.UseLoggerFactory(new LoggerFactory().AddConsole((message, logLevel) => logLevel == LogLevel.Debug && message.StartsWith("Microsoft.EntityFrameworkCore.Database.Command"))); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); foreach (var entityType in modelBuilder.Model .GetEntityTypes() .Where(e => typeof(IAuditableEntity) .IsAssignableFrom(e.ClrType))) { modelBuilder.Entity(entityType.ClrType) .Property<DateTimeOffset?>(AuditableShadowProperties.CreatedDateTime); modelBuilder.Entity(entityType.ClrType) .Property<DateTimeOffset?>(AuditableShadowProperties.ModifiedDateTime); } } public override int SaveChanges() { var auditEntries = OnBeforeSaveChanges(); var result = base.SaveChanges(); OnAfterSaveChanges(auditEntries); return result; } private IList<AuditEntry> OnBeforeSaveChanges() { var auditEntries = new List<AuditEntry>(); foreach (var entry in ChangeTracker.Entries<IAuditableEntity>()) { if (entry.State == EntityState.Detached || entry.State == EntityState.Unchanged) { continue; } var auditEntry = new AuditEntry(entry); auditEntries.Add(auditEntry); var now = DateTimeOffset.UtcNow; foreach (var property in entry.Properties) { var propertyName = property.Metadata.Name; if (propertyName == nameof(IAuditableEntity.Hash)) { continue; } if (property.IsTemporary) { // It's an auto-generated value and should be retrieved from the DB after calling the base.SaveChanges(). auditEntry.AuditProperties.Add(new AuditProperty(propertyName, null, true, property)); continue; } switch (entry.State) { case EntityState.Added: entry.Property(AuditableShadowProperties.CreatedDateTime).CurrentValue = now; auditEntry.AuditProperties.Add(new AuditProperty(propertyName, property.CurrentValue, false, property)); break; case EntityState.Modified: auditEntry.AuditProperties.Add(new AuditProperty(propertyName, property.CurrentValue, false, property)); entry.Property(AuditableShadowProperties.ModifiedDateTime).CurrentValue = now; break; } } } return auditEntries; } private void OnAfterSaveChanges(IList<AuditEntry> auditEntries) { foreach (var auditEntry in auditEntries) { foreach (var auditProperty in auditEntry.AuditProperties.Where(x => x.IsTemporary)) { // Now we have the auto-generated value from the DB. auditProperty.Value = auditProperty.PropertyEntry.CurrentValue; auditProperty.IsTemporary = false; } auditEntry.EntityEntry.Property(nameof(IAuditableEntity.Hash)).CurrentValue = auditEntry.AuditProperties.ToDictionary(x => x.Name, x => x.Value).GenerateObjectHash(); } base.SaveChanges(); } } }
public override int SaveChanges() { var auditEntries = OnBeforeSaveChanges(); var result = base.SaveChanges(); OnAfterSaveChanges(auditEntries); return result; }
if (property.IsTemporary) { // It's an auto-generated value and should be retrieved from the DB after calling the base.SaveChanges(). auditEntry.AuditProperties.Add(new AuditProperty(propertyName, null, true, property)); continue; }
همین مقدار تنظیم، برای محاسبه و به روز رسانی خودکار فیلد هش، کفایت میکند.
روش بررسی اصالت یک موجودیت
در متد زیر، روش محاسبهی هش واقعی یک موجودیت دریافت شدهی از بانک اطلاعاتی را توسط متد الحاقی GenerateEntityHash مشاهده میکنید. اگر این هش واقعی (بر اساس مقادیر فعلی این ردیف که حتی ممکن است به صورت دستی و خارج از برنامه تغییر کرده باشد)، با مقدار Hash ثبت شدهی پیشین در آن ردیف یکی بود، اصالت این ردیف تائید خواهد شد:
private static void CheckRow1IsAuthentic() { using (var context = new BloggingContext()) { var blog1 = context.Blogs.Single(x => x.BlogId == 1); var entityHash = context.GenerateEntityHash(blog1, propertyToIgnore: nameof(IAuditableEntity.Hash)); var dbRowHash = blog1.Hash; Console.WriteLine($"entityHash: {entityHash}\ndbRowHash: {dbRowHash}"); if (entityHash == dbRowHash) { Console.WriteLine("This row is authentic!"); } else { Console.WriteLine("This row is tampered outside of the application!"); } } }
entityHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo= dbRowHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo= This row is authentic!
اکنون بانک اطلاعاتی را خارج از برنامه، مستقیما دستکاری میکنیم و برای مثال Url اولین ردیف را تغییر میدهیم:
در ادامه یکبار دیگر برنامه را اجرا خواهیم کرد:
entityHash: tdiZhKMJRnROGLLam1WpldA0fy/CbjJaR2Y2jNU9izk= dbRowHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo= This row is tampered outside of the application!
به علاوه باید درنظر داشت، محاسبهی این هش بدون خود برنامه، کار سادهای نیست. به همین جهت به روز رسانی دستی آن تقریبا غیرممکن است؛ خصوصا اگر متد GenerateObjectHash، کمی با پیچ و تاب بیشتری نیز تهیه شود.
چگونه وضعیت اصالت تعدادی ردیف را بررسی کنیم؟
مثال قبل، در مورد روش بررسی اصالت یک تک ردیف بود. کوئری زیر روش محاسبهی فیلد جدید IsAuthentic را در بین لیستی از ردیفها نمایش میدهد:
var blogs = (from blog in context.Blogs.ToList() // Note: this `ToList()` is necessary here for having Shadow properties values, otherwise they will considered `null`. let computedHash = context.GenerateEntityHash(blog, nameof(IAuditableEntity.Hash)) select new { blog.BlogId, blog.Url, RowHash = blog.Hash, ComputedHash = computedHash, IsAuthentic = blog.Hash == computedHash }).ToList();
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: EFCoreRowIntegrity.zip
جهت اینکار یک پروژه از نوع class library ایجاد کنید. فایل class1.cs را که به طور پیش فرض ایجاد میشود، حذف کنید و رفرنسهای Microsoft.Web.Management.dll و Microsoft.Web.Administration.dll را از مسیر زیر اضافه کنید:
\Windows\system32\inetsrv
در مرحله بعدی در تب Build Events کد زیر را در بخش Post-build event command line اضافه کنید. این کد باعث میشود بعد از هر بار کامپایل پروژه، به طور خودکار در GAC ثبت شود:
call "%VS80COMNTOOLS%\vsvars32.bat" > NULL gacutil.exe /if "$(TargetPath)"
نکته:در صورتی که از VS2005 استفاده میکنید در تب Debug در قسمت Start External Program مسیر زیر را قرار بدهید. اینکار برای تست و دیباگینگ پروژه به شما کمک خواهد کرد. این تنظیم شامل نسخههای اکسپرس نمیشود.\windows\system32\inetsrv\inetmgr.exe
ساخت یک Module Provider
رابطهای کاربری IIS همانند هسته و کل سیستمش، ماژولار و قابل خصوصی سازی است. رابط کاربری، مجموعهای از ماژول هایی است که میتوان آنها را حذف یا جایگزین کرد. تگ ورودی یا معرفی برای هر UI یک module provider است. خیلی خودمانی، تگ ماژول پروایدر به معرفی یک UI در IIS میپردازد. لیستی از module providerها را میتوان در فایل زیر در تگ بخش <modules> پیدا کرد.
%windir%\system32\inetsrv\Administration.config
در اولین گام یک کلاس را به اسم imageCopyrightUIModuleProvider.cs ایجاد کرده و سپس آنرا به کد زیر، تغییر میدهیم. کد زیر با استفاده از ModuleDefinition یک نام به تگ Module Provider داده و کلاس imageCopyrightUI را که بعدا تعریف میکنیم، به عنوان مدخل entry رابط کاربری معرفی کرده:
using System; using System.Security; using Microsoft.Web.Management.Server; namespace IIS7Demos { class imageCopyrightUIProvider : ModuleProvider { public override Type ServiceType { get { return null; } } public override ModuleDefinition GetModuleDefinition(IManagementContext context) { return new ModuleDefinition(Name, typeof(imageCopyrightUI).AssemblyQualifiedName); } public override bool SupportsScope(ManagementScope scope) { return true; } } }
با ارث بری از کلاس module provider، سه متد بازنویسی میشوند که یکی از آن ها SupportsScope هست که میدان عمل پروایدر را مشخص میکند، مانند اینکه این پرواید در چه میدانی باید کار کند که میتواند سه گزینهی server,site,application باشد. در کد زیر مثلا میدان عمل application انتخاب شده است ولی در کد بالا با برگشت مستقیم true، همهی میدان را جهت پشتیبانی از این پروایدر اعلام کردیم.
public override bool SupportsScope(ManagementScope scope) { return (scope == ManagementScope.Application) ; }
حالا که پروایدر (معرف رابط کاربری به IIS) تامین شده، نیاز است قلب کار یعنی ماژول معرفی گردد. اصلیترین متدی که باید از اینترفیس ماژول پیاده سازی شود متد initialize است. این متد جایی است که تمام عملیات در آن رخ میدهد. در کلاس زیر imageCopyrightUI ما به معرفی مدخل entry رابط کاربری میپردازیم. در سازندههای این متد، پارامترهای نام، صفحه رابط کاربری وتوضیحی در مورد آن است. تصویر کوچک و بزرگ جهت آیکن سازی (در صورت عدم تعریف آیکن، چرخ دنده نمایش داده میشود) و توصیفهای بلندتر را نیز شامل میشود.
internal class imageCopyrightUI : Module { protected override void Initialize(IServiceProvider serviceProvider, ModuleInfo moduleInfo) { base.Initialize(serviceProvider, moduleInfo); IControlPanel controlPanel = (IControlPanel)GetService(typeof(IControlPanel)); ModulePageInfo modulePageInfo = new ModulePageInfo(this, typeof(imageCopyrightUIPage), "Image Copyright", "Image Copyright",Resource1.Visual_Studio_2012,Resource1.Visual_Studio_2012); controlPanel.RegisterPage(modulePageInfo); } }
شیء ControlPanel مکانی است که قرار است آیکن ماژول نمایش داده شود. شکل زیر به خوبی نام همه قسمتها را بر اساس نام کلاس و اینترفیس آنها دسته بندی کرده است:
پس با تعریف این کلاس جدید ما روی صفحهی کنترل پنل IIS، یک آیکن ساخته و صفحهی رابط کاربری را به نام imageCopyrightUIPage، در آن ریجستر میکنیم. این کلاس را پایینتر شرح دادهایم. ولی قبل از آن اجازه بدهید تا انواع کلاس هایی را که برای ساخت صفحه کاربرد دارند، بررسی نماییم. در این مثال ما با استفاده از پایهایترین کلاس، سادهترین نوع صفحه ممکن را خواهیم ساخت. 4 کلاس برای ساخت یک صفحه وجود دارند که بسته به سناریوی کاری، شما یکی را انتخاب میکنید.
ModulePage | شامل اساسیترین متدها و سورسها شده و هیچگونه رابط کاری ویژهای را در اختیار شما قرار نمیدهد. تنها یک صفحهی خام به شما میدهد که میتوانید از آن استفاده کرده یا حتی با ارث بری از آن، کلاسهای جدیدتری را برای ساخت صفحات مختلف و ویژهتر بسازید. در حال حاضر که هیچ کدام از ویژگیهای IIS فعلی از این کلاس برای ساخت رابط کاربری استفاده نکردهاند. |
ModuleDialogPage | یک صفحه شبیه به دیالوگ را ایجاد میکند و شامل دکمههای Apply و Cancel میشود به همراه یک سری متدهای اضافیتر که اجازهی override کردن آنها را دارید. همچنین یک سری از کارهایی چون refresh و از این دست عملیات خودکار را نیز انجام میدهد. از نمونه رابطهایی که از این صفحات استفاده میکنند میتوان machine key و management service را اسم برد. |
ModulePropertiesPage | این صفحه یک رابط کاربری را شبیه پنجره property که در ویژوال استادیو وجود دارد، در دسترس شما قرار میدهد. تمام عناصر آن در یک حالت گرید grid لیست میشوند. از نمونههای موجود میتوان به CGI,ASP.Net Compilation اشاره کرد. |
ModuleListPage | این کلاس برای مواقعی کاربرد دارد که شما قرار است لیستی از آیتمها را نشان دهید. در این صفحه شما یک ListView دارید که میتوانید عملیات جست و جو، گروه بندی و نحوهی نمایش لیست را روی آن اعمال کنید. |
public sealed class imageCopyrightUIPage : ModulePage { public string message; public bool featureenabled; public string color; ComboBox _colCombo = new ComboBox(); TextBox _msgTB = new TextBox(); CheckBox _enabledCB = new CheckBox(); public imageCopyrightUIPage() { this.Initialize(); } void Initialize() { Label crlabel = new Label(); crlabel.Left = 50; crlabel.Top = 100; crlabel.AutoSize = true; crlabel.Text = "Enable Image Copyright:"; _enabledCB.Text = ""; _enabledCB.Left = 200; _enabledCB.Top = 100; _enabledCB.AutoSize = true; Label msglabel = new Label(); msglabel.Left = 150; msglabel.Top = 130; msglabel.AutoSize = true; msglabel.Text = "Message:"; _msgTB.Left = 200; _msgTB.Top = 130; _msgTB.Width = 200; _msgTB.Height = 50; Label collabel = new Label(); collabel.Left = 160; collabel.Top = 160; collabel.AutoSize = true; collabel.Text = "Color:"; _colCombo.Left = 200; _colCombo.Top = 160; _colCombo.Width = 50; _colCombo.Height = 90; _colCombo.Items.Add((object)"Yellow"); _colCombo.Items.Add((object)"Blue"); _colCombo.Items.Add((object)"Red"); _colCombo.Items.Add((object)"White"); Button apply = new Button(); apply.Text = "Apply"; apply.Click += new EventHandler(this.applyClick); apply.Left = 200; apply.AutoSize = true; apply.Top = 250; Controls.Add(crlabel); Controls.Add(_enabledCB); Controls.Add(collabel); Controls.Add(_colCombo); Controls.Add(msglabel); Controls.Add(_msgTB); Controls.Add(apply); } public void ReadConfig() { try { ServerManager mgr; ConfigurationSection section; mgr = new ServerManager(); Configuration config = mgr.GetWebConfiguration( Connection.ConfigurationPath.SiteName, Connection.ConfigurationPath.ApplicationPath + Connection.ConfigurationPath.FolderPath); section = config.GetSection("system.webServer/imageCopyright"); color = (string)section.GetAttribute("color").Value; message = (string)section.GetAttribute("message").Value; featureenabled = (bool)section.GetAttribute("enabled").Value; } catch { } } void UpdateUI() { _enabledCB.Checked = featureenabled; int n = _colCombo.FindString(color, 0); _colCombo.SelectedIndex = n; _msgTB.Text = message; } protected override void OnActivated(bool initialActivation) { base.OnActivated(initialActivation); if (initialActivation) { ReadConfig(); UpdateUI(); } } private void applyClick(Object sender, EventArgs e) { try { UpdateVariables(); ServerManager mgr; ConfigurationSection section; mgr = new ServerManager(); Configuration config = mgr.GetWebConfiguration ( Connection.ConfigurationPath.SiteName, Connection.ConfigurationPath.ApplicationPath + Connection.ConfigurationPath.FolderPath ); section = config.GetSection("system.webServer/imageCopyright"); section.GetAttribute("color").Value = (object)color; section.GetAttribute("message").Value = (object)message; section.GetAttribute("enabled").Value = (object)featureenabled; mgr.CommitChanges(); } catch { } } public void UpdateVariables() { featureenabled = _enabledCB.Checked; color = _colCombo.Text; message = _msgTB.Text; } }
mgr.CommitChanges();
%vs110comntools%\vsvars32.bat
GACUTIL /l ClassLibrary1
<add name="imageCopyrightUI" type="ClassLibrary1.imageCopyrightUIProvider, ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d0b3b3b2aa8ea14b"/>
%windir%\system32\inetsrv\config\administration.config
از آنجا که این مقاله طولانی شده است، باقی موارد ویرایشی روی این UI را در مقاله بعدی بررسی خواهیم کرد.
DSL یا Domain Specific languages به معنی زبانهایی با دامنه محدود است که برای اهداف خاصی نوشته میشوند و تنها بر روی یک جنبه از هدف تمرکز دارند. این زبانها به شما اجازه نمیدهند که یک برنامه را به طور کامل با آن بنویسید. بلکه به شما اجازه میدهند به هدفی که برای آن نوشته شدهاند، برسید. یکی از این زبانها همان css هست که با آن کار میکنید. این زبان به صورت محدود تنها بر روی یک جنبه و آن، تزئین سازی المانهای وب، تمرکز دارد. در وقع مثل زبان سی شارپ همه منظوره نیست و محدودهای مشخص برای خود دارد. به این نوع از زبانهای DSL، نوع اکسترنال هم میگویند. چون زبانی مستقل برای خود است و به زبان دیگری وابستگی ندارد. ولی در یک زبان اینترنال، وابستگی به زبان دیگری وجود دارد. مثل Fluent Interfaceها که به ما شیوه آسانی از دسترسی به جنبههای یک شیء را میدهد. برای آشنایی هر چه بیشتر با این زبانها و ساختار آن، کتاب Domain Specific languages نوشته آقای مارتین فاولر توصیه میشود.
Groovy یک زبان شیء گرای DSL هست که برای پلتفرم جاوا ساخته شده است. برای اطلاعات بیشتر در مورد این زبان، صفحه ویکی ، میتواند مفید واقع شود.
از دیرباز سیستمهای Ant و Maven وجود داشتند و کار آنها اتوماسیون بعضی اعمال بود. ولی بعد از مدتی سیستم Gradle یا جمع کردن نقاط قوت آنها و افزودن ویژگیهای قدرتمندتری به خود، پا به میدان گذاشت تا راحتی بیشتری را برای برنامه نویس فراهم کند. از ویژگیهای گریدل میتوان داشتن زبان گرووی اشاره کرده که قدرت بیشتری را نسبت به سایر سیستمها داشت و مزیت مهم دیگر این بود که انعطاف بالایی را جهت افزودن پلاگینها داشت و گوگل با استفاده از این قابلیت، پشتیبانی از گریدل را در اندروید استادیو نیز گنجاند تا راحتی بیشتری را در اتوماسیون وظایف سیستمی ایجاد کند. در واقع آنچه شما در سیستم گریدل کار میکنید و اطلاعات خود را با آن کانفیگ میکنید، پلاگینی است که از سمت گوگل در اختیار شما قرار گرفته است و در مواقع خاص این وظایف توسط پلاگینها اجرا میشوند.
گریدل به راحتی از سایت رسمی آن قابل دریافت است و میتوان آن را در پروژههای جاوایی که مدنظر شماست، دریافت کنید و با استفاده از خط فرمان، با آن تعامل کنید. هر چند امروزه اکثر ویراستارهای جاوا از آن پشتیبانی میکنند.
گریدل یک ماهیت توصیفی دارد که شما تنها لازم است اعمالی را برای آن توصیف کنید تا بقیه کارها را انجام دهد. گریدل در پشت صحنه از یک "گراف جهت دار بدون دور" Directed Acycllic Graph یا به اختصار DAG استفاده میکند و طبق آن ترتیب وظایف یا taskها را دانسته و آنها را اجرا میکند. گریدل با این DAG، سه فاز آماده سازی، پیکربندی و اجرا را انجام میدهد.
- در مرحله آماده سازی ما به گریدل میگوییم چه پروژه یا پروژههایی نیاز به بیلد شدن دارند. در اندروید استادیو، این مرحله در فایل settings.gradle انجام میشود؛ شما در این فایل مشخص میکنید چه پروژههای نیاز به بیلد شدن توسط گریدل دارند. ساختار این فایل به این شکل است:
include ':ActiveAndroid-master', ':app', ':dbutilities'
-
در اولین مرحله انتظار دارد که فایل settings در دایرکتوری جاری باشد و اگر آن را پیدا کرد آن را مورد استفاده قرار میدهد؛ در غیر اینصورت مرحله بعدی را آغاز میکند.
- در مرحله دوم، در این دایرکتوری به دنبال دایرکتوری به نام master میگردد و اگر در آن هم یافت نکرد مرحله سوم را آغاز میکند.
- در مرحله سوم، جست و جو در دایرکتوری والد انجام میشود
- چنانچه این فایل را در هیچ یک از احتمالات بالا نیابد، همین پروژه جاری را تشخیص خواهد داد.
include ':ActiveAndroid-master', ':app', ':dbutilities' project('dbutilities').projectDir=new File(settingsDir,'../dir1/dir2');
-
در مرحله پیکربندی، وظایف یا taskها را معرفی میکنیم. این عمل پیکربندی توسط فایل build.gradle که برای پروژه اصلی و هر زیر پروژهای که مشخص شدهاند، صورت میگیرد. در این فایل شما میتوانید خواص و متدهایی را تعریف و و ظایفی را مشخص کنید.
در پروژه اصلی، فایل BuildGradle شامل خطوط زیر است:
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.5.0' } } allprojects { repositories { jcenter() } }
در مرحله اجرا هم این وظایف را اجرا میکنیم. تمامی این سه عملیات توسط فایل و دستوری به نام gradlew که برگرفته از gradleWrapper میباشد انجام میشود. اگر در ترمینال اندروید استادیو این عبارت را تایپ کنید، میتوانید در ادامه دستور پیامهای مربوط به این عملیات را ببینید و ترتیب اجرای فازها را مشاهده کنید.
بیایید یک task را تعریف کنیم
task mytask <<{ println ".net tips task in config phase" }
gradlew mytask
gradlew --info mytask
اگر بخواهید خودتان دستی یک تسک پیکربندی را به یک تسک اجرایی تبدیل کنید، میتوانید متد doLast را صدا بزنید. کد زیر را توسط gradlew اجرا کنید؛ به همراه اطلاعات verbose تا ببینید که هر کدام از پیامها در کدام بخش چاپ میشوند. پیام اول در فاز پیکربندی و پیام دوم در فاز اجرایی چاپ میشوند.
task mytask { println ".net tips task in config phase" doLast{ println ".net tips task in exe phase" } }
یکی از کارهایی که در یک تسک میتوانید انجام دهید این است که آن را به یک تسک دیگر وابسته کنید. به عنوان مثال ما قصد داریم بعد از تسک mytask1، تسک my task2 اجرا شود و زمان پایان تسک mytask1 را در خروجی نمایش دهیم. برای اینکار باید بین تسکها یک وابستگی ایجاد شود و سپس با متد doLast کد خودمان را اجرایی نماییم. البته توجه داشته باشید که این وابستگیها تنها به تسکهای داخل فایل گریدل انجام میشود و نه تسکهای پلاگینها یا وابستگی هایی که تعریف میکنیم.
task mytask1 << { println ".net tips is the best" } task mytask2() { dependsOn mytask1 doLast{ Date time=Calendar.getInstance().getTime(); SimpleDateFormat formatter=new SimpleDateFormat("HH:mm:ss , YYYY/MM/dd"); println "mytask1 is done at " + formatter.format(time); } }
gradlew --info mytask2
Executing task ':app:mytask1' (up-to-date check took 0.003 secs) due to: Task has not declared any outputs. خروجی تسک شماره یک .net tips is the best :app:mytask1 (Thread[main,5,main]) completed. Took 0.046 secs. :app:mytask2 (Thread[main,5,main]) started. :app:mytask2 Executing task ':app:mytask2' (up-to-date check took 0.0 secs) due to: Task has not declared any outputs. خروجی تسک شماره دو mytask1 is done at 04:03:09 , 2016/07/07 :app:mytask2 (Thread[main,5,main]) completed. Took 0.075 secs. BUILD SUCCESSFUL
در گریدل مخالف doLast یعنی doFirst را نیز داریم ولی عملگر جایگزینی برای آن وجود ندارد و مستقیما باید آن را پیاده سازی کنید. خود گریدل به طور پیش فرض نیز تسکهای آماده ای نیز دارد که میتوانید در مستندات آن بیابید. به عنوان مثال یکی از تسکهای مفید و کاربردی آن تسک کپی کردن هست که از طریق آن میتوانید فایلی یا فایلهایی را از یک مسیر به مسیر دیگر کپی کنید. برای استفاده از چنین تسکهایی، باید تسکهای خود را به شکل زیر به شیوه اکشن بنویسید:
task mytask(type:Copy) { dependsOn mytask1 doLast{ from('build/apk') { include '**/*.apk' } into '.' } }
برای نمایش تسکهای موجود میتوانید از گریدل درخواست کنید که لیست تمامی تسکهای موجود را به شما نشان دهد. برای اینکار میتوانید دستور زیر را صدا کنید:
gradlew --info tasks
Other tasks ----------- clean jarDebugClasses jarReleaseClasses mytask mytask2 transformResourcesWithMergeJavaResForDebugUnitTest transformResourcesWithMergeJavaResForReleaseUnitTest
task mytask(type:Copy) { description "copy apk files to root directory" dependsOn mytask1 doLast{ from('build/apk') { include '**/*.apk' } into '.' } }
یکی دیگر از نکات جالب در مورد گریدل این است که میتواند برای شما callback ارسال کند. بدین صورت که اگر اتفاقی خاصی افتاد، تسک خاصی را اجرا کند. به عنوان مثال ما در کد پایین تسکی را ایجاد کردهایم که به ما این اجازه را میدهد، هر موقع تسکی در مرحله پیکربندی به بیلد اضافه میشود، تسک ما هم اجرا شود و نام تسک اضافه شده به بیلد را چاپ میکند.
tasks.whenTaskAdded{ task-> println "task is added $task.name" }
گریدل امکانات دیگری چون بررسی استثناءها و ایجاد استثناءها را هم پوشش میدهد که میتوانید در این صفحه آن را پیگیری کنید.
Gradle Wrapper
گریدل در حال حاضر مرتبا در حال تغییر و به روز رسانی است و اگر بخواهیم مستقیما با گریدل کار کنیم ممکن است که به مشکلاتی که در نسخه بندی است برخورد کنیم. از آنجا که هر پروژهای که روی سیستم شما قرار بگیرد از نسخهای متفاوتی از گریدل استفاده کند، باعث میشود که نتوانید نسخه مناسبی از گریدل را برای سیستم خود دانلود کنید. بدین جهت wrapper ایجاد شد تا دیگر نیازی به نصب گریدل پیدا نکنید. wrapper در هر پروژه میداند که که به چه نسخهای از گریدل نیاز است. پس موقعی که شما دستور gradlew را صدا میزنید در ویندوز فایل gradlew.bat صدا زده شده و یا در لینوکس و مک فایل شِل اسکریپت gradlew صدا زده میشود و wrapper به خوبی میداند که به چه نسخهای از گریدل برای اجرا نیاز دارد و آن را از طریق دانلود فراهم میکند. اگر همینک دایرکتوری والد پروژه اندرویدی خود را نگاه کنید میتوانید این دو فایل را ببینید.
از آنجا که خود اندروید استادیو به ساخت wrapper اقدام میکند، شما راحت هستید. ولی اگر دوست دارید خودتان برای پروژهای wrapper تولید کنید، مراحل زیر را دنبال کنید:
برای ایجاد wrapper توسط خودتان باید گریدل را دانلود و روی سیستم نصب کنید و سپس دستور زیر را صادر کنید:
gradle wrapper --gradle-version 2.4
اگر میخواهید ببینید wrapper که اندروید استادیو شما دارد چه نسخه از گردیل را صدا میزند مسیر را از دایرکتوری پروژه دنبال کنید و فایل زیر را بگشایید:
\gradle\wrapper\gradle-wrapper.properties
اینها فقط مختصراتی از آشنایی با نحوه عملکر گریدل برای داشتن دیدی روشنتر نسبت به آن بود. برای آشنایی بیشتر با گریدل، باید مستندات رسمی آن را دنبال کنید.
در ادامه بجای این روشها، از SignalR برای محاسبه تعداد کاربران آنلاین و همچنین به روز رسانی بلادرنگ این عدد در سمت کاربر، استفاده خواهیم کرد.
تشخیص اتصال و قطع اتصال کاربران در SignalR
زیر ساختهای کلاس Hub موجود در SignalR، دارای متدهای ردیابی اتصال (OnConnected)، قطع اتصال (OnDisconnected) و یا برقراری مجدد اتصال کاربران (OnReconnected) هستند. با بازنویسی این متدها میتوان به تخمین بسیار دقیقی از تعداد کاربران آنلاین یک سایت رسید.
پیشنیازهای بحث
پیشنیازهای این بحث با مطلب «مثال - نمایش درصد پیشرفت عملیات توسط SignalR» یکی است. برای مثال نحوه دریافت وابستگیها، تنظیمات فایل global.asax و افزودن اسکریپتها، تفاوتی با مثال یاد شده ندارند.
تعریف هاب کاربران آنلاین برنامه
using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.SignalR; namespace SignalR05.Common { public class OnlineUsersHub : Hub { public static readonly ConcurrentDictionary<string, string> OnlineUsers = new ConcurrentDictionary<string, string>(); public void UpdateUsersOnlineCount() { // آی پی معرف یک کاربر است // اما کانکشن آی دی معرف یک برگه جدید در مرورگر او است // هر کاربر میتواند چندین برگه را به یک سایت گشوده یا ببندد var ipsCount = OnlineUsers.Select(x => x.Value).Distinct().Count(); this.Clients.All.updateUsersOnlineCount(ipsCount); } /// <summary> /// اگر کاربران اعتبار سنجی شدهاند بهتر است از /// this.Context.User.Identity.Name /// بجای آی پی استفاده شود /// </summary> protected string GetUserIpAddress() { object environment; if (!Context.Request.Items.TryGetValue("owin.environment", out environment)) return null; object serverRemoteIpAddress; if (!((IDictionary<string, object>)environment).TryGetValue("server.RemoteIpAddress", out serverRemoteIpAddress)) return null; return serverRemoteIpAddress.ToString(); } public override Task OnConnected() { var ip = GetUserIpAddress(); OnlineUsers.TryAdd(this.Context.ConnectionId, ip); UpdateUsersOnlineCount(); return base.OnConnected(); } public override Task OnReconnected() { var ip = GetUserIpAddress(); OnlineUsers.TryAdd(this.Context.ConnectionId, ip); UpdateUsersOnlineCount(); return base.OnReconnected(); } public override Task OnDisconnected() { // در این حالت ممکن است مرورگر کاملا بسته شده باشد // یا حتی صرفا یک برگه مرورگر از چندین برگه متصل به سایت بسته شده باشند string ip; OnlineUsers.TryRemove(this.Context.ConnectionId, out ip); UpdateUsersOnlineCount(); return base.OnDisconnected(); } } }
در اینجا، هم به IP کاربر و هم به ConnectionId او نیاز است. از این جهت که هر ConnectionId، معرف یک برگه جدید باز شده در مرورگر کاربر است. اگر صرفا IPها را پردازش کنیم، با بسته شدن یکی از چندین برگه مرورگر او که اکنون به سایت متصل هستند، آمار او را از دست خواهیم داد. این کاربر هنوز چندین برگه باز دیگر را دارد که با سایت در ارتباط هستند، اما چون IP او را از لیست حذف کردهایم (در نتیجه بسته شدن یکی از برگهها)، آمار کلی شخص را نیز از دست خواهیم داد. بنابراین هر دوی IP و ConnectionIdها باید پردازش شوند.
اگر برنامه شما دارای اعتبارسنجی است (یک صفحه لاگین دارد)، بهتر است بجای IP از this.Context.User.Identity.Name استفاده کنید.
کدهای سمت کلاینت نمایش آمار کاربران
<html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script> <script src="Scripts/jquery.signalR-1.1.3.min.js" type="text/javascript"></script> <script type="text/javascript" src='<%= ResolveClientUrl("~/signalr/hubs") %>'></script> </head> <body> <form id="form1" runat="server"> online users count: <span id="usersCount"></span> </form> <script type="text/javascript"> $(function () { $.connection.hub.logging = true; var onlineUsersHub = $.connection.onlineUsersHub; onlineUsersHub.client.updateUsersOnlineCount = function (count) { $('#usersCount').text(count); }; $.connection.hub.start(); }); </script> </body> </html>
کدهای کامل این مثال را از اینجا نیز میتوانید دریافت کنید:
SignalR05.zip
الگوی طراحی Factory Method به همراه مثال
عناوین :
· تعریف Factory Method
· دیاگرام UML
· شرکت کنندگان در UML
· مثالی از Factory Pattern در #C
تعریف الگوی Factory Method :
این الگو پیچیدگی ایجاد اشیاء برای استفاده کننده را پنهان میکند. ما با این الگو میتوانیم بدون اینکه کلاس دقیق یک شیئ را مشخص کنیم آن را ایجاد و از آن استفاده کنیم. کلاینت ( استفاده کننده ) معمولا شیئ واقعی را ایجاد نمیکند بلکه با یک واسط و یا کلاس انتزاعی (Abstract) در ارتباط است و کل مسئولیت ایجاد کلاس واقعی را به Factory Method میسپارد. کلاس Factory Method میتواند استاتیک باشد . کلاینت معمولا اطلاعاتی را به متدی استاتیک از این کلاس میفرستد و این متد بر اساس آن اطلاعات تصمیم میگیرید که کدام یک از پیاده سازیها را برای کلاینت برگرداند.
از مزایای این الگو این است که اگر در نحوه ایجاد اشیاء تغییری رخ دهد هیچ نیازی به تغییر در کد کلاینتها نخواهد بود. در این الگو اصل DIP از اصول پنجگانه SOLID به خوبی رعایت میشود چون که مسئولیت ایجاد زیرکلاسها از دوش کلاینت برداشته میشود.
دیاگرام UML :
در شکل زیر دیاگرام UML الگوی Factory Method را مشاهده میکنید.
شرکت کنندگان در این الگو به شرح زیل هستند :
- Iproduct یک واسط است که هر کلاینت از آن استفاده میکند. در اینجا کلاینت استفاده کننده نهایی است مثلا میتواند متد main یا هر متدی در کلاسی خارج از این الگو باشد. ما میتوانیم پیاده سازیهای مختلفی بر حسب نیاز از واسط Iproduct ایجاد کنیم.
- ConcreteProduct یک پیاده سازی از واسط Iproduct است ، برای این کار بایستی کلاس پیاده سازی (ConcreteProduct) از این واسط (IProduct) مشتق شود.
- Icreator واسطیست که Factory Method را تعریف میکند. پیاده ساز این واسط بر اساس اطلاعاتی دریافتی کلاس صحیح را ایجاد میکند. این اطلاعات از طریق پارامتر برایش ارسال میشوند.همانطور که گفتیم این عملیات بر عهده پیاده ساز این واسط است و ما در این نمودار این وظیفه را فقط بر عهده ConcreteCreator گذاشته ایم که از واسط Icreator مشتق شده است.
پیاده سازی UMLفوق به صورت زیر است:
در ابتدا کلاس واسط IProduct تعریف شده است.
interface IProduct { // در اینجا برحسب نیاز فیلدها و یا امضای متدها قرار میگیرند }
در این مرحله ما پند پیاده سازی از IProduct انجام میدهیم.
class ConcreteProductA : IProduct { // A پیاده سازی } class ConcreteProductB : IProduct { // B پیاده سازی }
abstract class Creator { // این متد بر اساس نوع ورودی انتخاب مناسب را انجام و باز میگرداند public abstract IProduct FactoryMethod(string type); }
class ConcreteCreator : Creator { public override IProduct FactoryMethod(string type) { switch (type) { case "A": return new ConcreteProductA(); case "B": return new ConcreteProductB(); default: throw new ArgumentException("Invalid type", "type"); } } }
برای روشنتر شدن موضوع ، یک مثال کاملتر ارائه داده میشود. در شکل زیر طراحی این برنامه نشان داده شده است.
کد برنامه به شرح زیل است :
خروجی اجرای برنامه فوق به شکل زیر است :using System; namespace FactoryMethodPatternRealWordConsolApp { internal class Program { private static void Main(string[] args) { VehicleFactory factory = new ConcreteVehicleFactory(); IFactory scooter = factory.GetVehicle("Scooter"); scooter.Drive(10); IFactory bike = factory.GetVehicle("Bike"); bike.Drive(20); Console.ReadKey(); } } public interface IFactory { void Drive(int miles); } public class Scooter : IFactory { public void Drive(int miles) { Console.WriteLine("Drive the Scooter : " + miles.ToString() + "km"); } } public class Bike : IFactory { public void Drive(int miles) { Console.WriteLine("Drive the Bike : " + miles.ToString() + "km"); } } public abstract class VehicleFactory { public abstract IFactory GetVehicle(string Vehicle); } public class ConcreteVehicleFactory : VehicleFactory { public override IFactory GetVehicle(string Vehicle) { switch (Vehicle) { case "Scooter": return new Scooter(); case "Bike": return new Bike(); default: throw new ApplicationException(string.Format("Vehicle '{0}' cannot be created", Vehicle)); } } } }
فایل این برنامه ضمیمه شده است، از لینک مقابل دانلود کنید FactoryMethodPatternRealWordConsolApp.zip
در مقالات بعدی مثالهای کاربردیتر و جامعتری از این الگو و الگوهای مرتبط ارائه خواهم کرد...
بررسی تغییرات Blazor 8x - قسمت سوم - روش ارتقاء برنامههای Blazor Server قدیمی به دات نت 8
همانطور که در این مطلب هم اشاره شد، حالت پیشفرض رندر در برنامههای Blazor 8x، فقط SSR است. بنابراین قسمتهای تعاملی تمام کامپوننتها (ثالث یا غیر ثالث) در این حالت کار نمیکنند؛ مگر اینکه:
- یکی از حالتهای رندر تعاملی را در بالاترین سطح ممکن فعال کنید (اضافه کردن صریح rendermode@ در فایل App.razor به کامپوننتهای HeadOutlet و Routes) تا تمام صفحات و کامپوننتهای برنامه از آن ارثبری کنند.
- یا rendermode@ را در حین تعریف المان کامپوننت، صراحتا ذکر کنید (حالت تعریف رندر جزیرهای).
- یا rendermode@ را در حین تعریف صفحهی جاری ذکر کنید تا تمام کامپوننتهای واقع در آن صفحه، از آن ارثبری کنند.
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(User applicationUser) { // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType var userIdentity = await CreateIdentityAsync(applicationUser, DefaultAuthenticationTypes.ApplicationCookie); // Add custom user claims here userIdentity.AddClaim(new Claim("emailaddress", applicationUser.Email)); return userIdentity; }
public virtual async Task<ActionResult> Login(LoginViewModel viewModel, string returnUrl) { // ... more code var user = await this._userManager.FindByNameAsync(viewModel.UserName); await this._userManager.GenerateUserIdentityAsync(user); return this.View(viewModel); }
_.For<IPrincipal>().Use(() => HttpContext.Current.User);
var useremail = this._principal.GetClaimValue("emailaddress");
در خیلی از مواقع workflowها به مرحلهای میرسند که احتیاج به دستوری از بیرون از فرآیند دارند. در هنگام انتظار، اگر به هر دلیلی workflow از حافظه حذف شود، امکان ادامه فرآیند وجود ندارد. اما میتوان با Persist (ذخیره) کردن آن، در زمان انتظار و فراخوانی مجدد آن در هنگام نیاز، این ریسک را برطرف نمود.
قصد دارم با این مثال، طریقه persist شدن یک workflow در زمانیکه نیاز به انتظار برای تایید دارد و فراخوانی آن از همان نقطه پس از تایید مربوطه را توضیح دهم.
ساختار اینترفیس کاربری ما WPF میباشد. پس در ابتدا یک پروژه از نوع WPF ایجاد میکنیم. اسم solution را PersistWF و اسم Project را PersistWF.UI انتخاب میکنیم.
در پروژه UI نام فایل MainWindow.xaml را به AddRequest.xaml تغییر میدهیم. همچنین اسم کلاس مربوطه را در codebehind
همین طور مقدار StartupUri را هم در app.xaml اصلاح میکنیم
StartupUri="AddRequest.xaml"
Reference های زیر رو هم به پروژه اضافه میکنیم
•System.Activities •System.Activities.DurableInstancing •System.Configuration •System.Data.Linq •System.Runtime.DurableInstancing •System.ServiceModel •System.ServiceModel.Activities •System.Workflow.ComponentModel •System.Runtime.DurableInstancing •System.Activities.DurableInstancing
قرار است کاربری ثبت نام کند، در فرایند ثبت، منتظر تایید یکی از مدیران قرار میگیرد. مدیر، لیست کاربران جدید را میبینید، یک کاربر را انتخاب میکند؛ مقادیر لازم را وارد میکند و سپس پروسه تایید را انجام میدهد که فراخوانی فرآیند مربوطه از همان قسمتیاست که منتظر تایید مانده است.
برای Persist کردن workflow از کلاس SqlWorkflowInstanceStore استفاده میکنم. این شی به connection ای به یک دیتابیس با یک ساختار معین احتیاج دارد. خوشبختانه اسکریپتهای مورد نیاز این ساختار در پوشه [Drive]:\Windows\Microsoft.NET\Framework\v4.0.30319\SQL\en وجود دارند. دو اسکریپت با نامهای SqlWorkflowInstanceStoreSchema و SqlWorkflowInstanceStoreLogic باید به ترتیب در دیتابیس اجرا شوند.
من یک دیتابیس با نام PersistWF ایجاد میکنم و اسکریپتها را بر روی آن اجرا میکنم. یک جدول هم برای نگهداری کاربران ثبت شده در همین دیتابیس ایجاد میکنم.
و شمایل دیتابیس ما پس از اجرا کردن اسکریپتها و ساختن جدول User بدین شکل است:
XAML زیر، ساختار فرم AddRequest میباشد که قرار است نقش UI برنامه را ایفا کند. آن را با XAMLهای پیش فرض عوض کنید.
<Window x:Class="PersistWF.UI.AddRequest" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="520" Width="550" Loaded="Window_Loaded"> <Grid MinWidth="300" MinHeight="100" Width="514"> <Label Height="30" Margin="5,10,10,10" Name="lblName" VerticalAlignment="Top" HorizontalAlignment="Left" Width="90" HorizontalContentAlignment="Right">Name:</Label> <Label Height="30" Margin="270,10,10,10" Name="lblPhone" VerticalAlignment="Top" HorizontalAlignment="Left" Width="90" HorizontalContentAlignment="Right">Phone Number:</Label> <Label Height="30" Margin="5,40,10,10" Name="lblEmail" VerticalAlignment="Top" HorizontalAlignment="Left" Width="90" HorizontalContentAlignment="Right">Email:</Label> <TextBox Height="25" Margin="100,10,10,10" Name="txtName" VerticalAlignment="Top" HorizontalAlignment="Left" Width="170" /> <TextBox Height="25" Margin="365,10,10,10" Name="txtPhone" VerticalAlignment="Top" HorizontalAlignment="Left" Width="100" /> <TextBox Height="25" Margin="100,40,10,10" Name="txtEmail" VerticalAlignment="Top" HorizontalAlignment="Left" Width="300" /> <Button Height="23" Margin="100,86,0,0" Name="brnRegister" VerticalAlignment="Top" HorizontalAlignment="Left" Width="70" Click="brnRegister_Click">Register</Button> <ListView x:Name="lstUsers" Margin="10,125,10,10" Height="145" VerticalAlignment="Top" ItemsSource="{Binding}" HorizontalContentAlignment="Center" SelectionChanged="lstUsers_SelectionChanged" > <ListView.View> <GridView> <GridViewColumn Header="Current User" Width="480"> <GridViewColumn.CellTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" Width="110"/> <TextBlock Text="{Binding Phone}" Width="70"/> <TextBlock Text="{Binding Email}" Width="130"/> <TextBlock Text="{Binding Status}" Width="70"/> <TextBlock Text="{Binding AcceptedBy}" Width="100"/> </StackPanel> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> </GridView> </ListView.View> </ListView> <Label Height="37" HorizontalAlignment="Stretch" Margin="10,272,5,10" Name="lblSelectedNotes" VerticalAlignment="Top" Visibility="Hidden" /> <Label Height="30" Margin="10,0,0,140" Name="lblAgent" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="40" HorizontalContentAlignment="Left" Visibility="Hidden">Admin Name:</Label> <TextBox Height="25" Margin="60,0,0,140" Name="txtAcceptedBy" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="190" Visibility="Hidden" /> <Button Height="25" Margin="270,0,0,140" Name="btnAccept" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="90" Click="btnAccept_Click" Visibility="Hidden">Accept</Button> <Label Height="27" HorizontalAlignment="Left" Margin="10,0,0,110" Name="lblEvent" VerticalAlignment="Bottom" Width="76">Event Log</Label> <ListBox Margin="12,0,5,12" Name="lstEvents" Height="100" VerticalAlignment="Bottom" FontStretch="Condensed" FontSize="10" /> </Grid> </Window>
اگر همه چیز مرتب باشد؛ ساختار فرم شما باید به این شکل
باشد
اکثر workflowها از activity معروف WrteLine استفاده میکنند که برای نمایش یک رشته به کار میرود. ما هم در workflow مثالمان از این Activity استفاده میکنیم. اما برای اینکه مقادیری که توسط این Activity ایجاد میشوند در کادر event log فرم خودمان نمایش داده شود؛ احتیاج داریم که یک TextWriter سفارشی برای خودمان ایجاد کنیم. اما قبل از آن یک کلاس static در پروژه ایجاد میکنیم که بتوانیم در هر قسمتی، به فرم دسترسی داشته باشیم.
کلاسی را با نام ApplicationInterface به پروژه اضافه کرده و یک Property استاتیک از جنس فرم AddRequest هم
برای آن تعریف میکنیم:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace PersistWF.UI { public static class ApplicationInterface { public static AddRequest _app { get; set; } } }
به Constructor کلاس موجود در فایل AddRequest.xaml.cs این خط کد رو اضافه میکنم
public AddRequest() { InitializeComponent(); ApplicationInterface._app = this; }
private void AddEvent(string szText) { lstEvents.Items.Add(szText); } public ListBox GetEventListBox() { return this.lstEvents; }
متد اول برای اضافه کردن یک event Log و متد دوم هم که کنسول لاگ را در اختیار درخواست کنندهاش قرار میدهد.
و حالا کلاس TextWriter سفارشیامان را مینویسیم. یک کلاس به نام ListBoxTextWriter به پروژه اضافه میکنیم که از TextWriter مشتق میشود و محتویات آنرا در زیر میبینید:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Controls; namespace PersistWF.UI { public class ListBoxTextWriter : TextWriter { const string textClosed = "This TextWriter must be opened before use"; private Encoding _encoding; private bool _isOpen = false; private ListBox _listBox; public ListBoxTextWriter() { // Get the static list box _listBox = ApplicationInterface._app.GetEventListBox(); if (_listBox != null) _isOpen = true; } public ListBoxTextWriter(ListBox listBox) { this._listBox = listBox; this._isOpen = true; } public override Encoding Encoding { get { if (_encoding == null) { _encoding = new UnicodeEncoding(false, false); } return _encoding; } } public override void Close() { this.Dispose(true); } protected override void Dispose(bool disposing) { this._isOpen = false; base.Dispose(disposing); } public override void Write(char value) { if (!this._isOpen) throw new ApplicationException(textClosed); ; this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(value.ToString()))); } public override void Write(string value) { if (!this._isOpen) throw new ApplicationException(textClosed); if (value != null) this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(value))); } public override void Write(char[] buffer, int index, int count) { String toAdd = ""; if (!this._isOpen) throw new ApplicationException(textClosed); ; if (buffer == null || index < 0 || count < 0) throw new ArgumentOutOfRangeException("buffer"); if ((buffer.Length - index) < count) throw new ArgumentException("The buffer is too small"); for (int i = 0; i < count; i++) toAdd += buffer[i]; this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(toAdd))); } } }
همان طور که میبینید کلاس ListBoxTextWriter از کلاس abstract TextWriter مشتق شده و پیاده سازی از متد Write را فراهم میکند تا یک رشته را به کنترل ListBox اضافه کنه. (البته سه تا از این متدها را Override میکنیم تا بتوانیم یک رشته، یک کاراکتر و یا آرایه ای از کاراکترها را به ListBox اضافه کنیم) در constructor پیشفرض از کلاس ApplicationInterface استفاده کردیم تا بتوانیم کنترل lstEvents را از فرم اصلی برنامه به دست بیاوریم. برای Add کردن از Dispatcher و متد BeginInvoke مرتبط با آن استفاده کردیم . این کار، متد را قادر میسازد حتی وقتیکه از یک thread متفاوت فراخوانی میشود، کار کند.
حالا میتوانیم از این کلاس، به عنوان مقدار خاصیت TextWriter برای WriteLine استفاده کنیم.
به کلاس ApplicationInterface برگردیم تا متد زیر را هم به آن اضافه کنیم
public static void AddEvent(String status) { if (_app != null) { new ListBoxTextWriter(_app.GetEventListBox()).WriteLine(status); } }
این هم از constructor دومی استفاده میکنه برای معرفی ListBox.
برای ارتباط با دیتابیس از LINQ to SQL استفاده میکنیم تا User رو ذخیره و بازیابی کنیم. به پروژه یک آیتم از نوع LINQ to SQL با نام UserData.dbml اضافه میکنیم. به دیتابیس متصل شده و جدول User رو به محیط Design میکشیم. در ادامه برای شی کلاس SQLWorkflowInstanceStore هم از همین Connectionstring استفاده میکنیم.
برای ایجاد workflow مورد نظر، به دو Activity سفارشی احتیاج داریم که باید خودمان ایجاد نماییم. یک پوشه با نام Activities به پروژه اضافه میکنم تا کلاسهای مورد نظر را آنجا ایجاد کنیم.
1. یک Activity برای ایجاد User
این Activity تعدادی پارامتر از نوع InArgument دارد که توسط آنها یک Instance از کلاس User ایجاد میکند و در حقیقت آن را به دیتابیس میفرستد و دخیره میکند. Connectionstring را هم میشود توسط یک آرگومان ورودی دیگر مقدار دهی کرد. یک آرگومان خروجی هم برای این Activity در نظر میگیریم تا User ایجاد شده را برگردانیم. روی پوشهی Activities کلیک راست میکنیم و Add - NewItem را انتخاب میکنیم. از لیست workflowها Template مربوط به CodeActivity را انتخاب کرده و یک CodeActivity با نام CreateUser ایجاد میکنیم
محتویات این کلاس را هم مانند زیر کامل میکنیم
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Activities; namespace PersistWF.UI.Activities { public sealed class CreateUser : CodeActivity { public InArgument<string> Name { get; set; } public InArgument<string> Email { get; set; } public InArgument<string> Phone { get; set; } public InArgument<string> ConnectionString { get; set; } public OutArgument<User> User { get; set; } protected override void Execute(CodeActivityContext context) { // ایجاد کاربر User user = new User(); user.Email = Email.Get(context); user.Name = Name.Get(context); user.Phone = Phone.Get(context); user.Status = "New"; user.WorkflowID = context.WorkflowInstanceId; UserDataDataContext db = new UserDataDataContext(ConnectionString.Get(context)); db.Users.InsertOnSubmit(user); db.SubmitChanges(); User.Set(context, user); } } }
متد Execute، توسط مقادیری که به عنوان پارامتر دریافت شده، یک شی از کلاس User ایجاد میکند و به کمک DataContext آنرا در دیتابیس دخیره کرده و در آخر User ذخیره شده را در اختیار پارامتر خروجی قرار میدهد.
1. یک Activity برای انتظار دریافت تایید
این Activity قرار است Workflow را Idle کند تا زمانیکه مدیر دستور تایید را با فراخوانی مجدد workflow از این همین
قسمت صادر نماید.
این Activity باید از NativeActivity مشتق شده و برای اینکه workflow را وادرا به معلق شدن کند کافیاست خاصیت CanInduceIdle را با مقدار برگشتی true , override کنیم.
مثل قسمت قبل یک CodeActivity ایجاد میکنیم. اینبار با نام WaitForAccept که محتویاتش را هم مانند زیر تغییر میدهیم.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Activities; using System.Workflow.ComponentModel; namespace PersistWF.UI.Activities { public sealed class WaitForAccept<T> : NativeActivity<T> { public WaitForAccept() :base() { } public string BookmarkName { get; set; } public OutArgument<T> Input { get; set; } protected override void Execute(NativeActivityContext context) { context.CreateBookmark(BookmarkName, new BookmarkCallback(this.Continue)); } private void Continue(NativeActivityContext context, Bookmark bookmark, object value) { Input.Set(context, (T)value); } protected override bool CanInduceIdle { get { return true; } } } }
ما User هایی را که به این نقطه رسیدنْ نمایش میدهیم. مدیر اونها را دیده و با مقدار دهی فیلد AcceptedBy، آن User را از اینجا به workflow میفرستد و ما user وارد شده را در ادامهی فرآیند Accept میکنیم.
برای ایجاد workflow هم میتوانید از designer استفاده کنید و هم میتوانید کد مربوط به workflow را پیاده سازی کنید.
برای پیاده سازی از طریق کد، یک کلاس با نام UserWF ایجاد میکنیم و محتویات workflow را مانند زیر پیاده سازی خواهیم کرد:
using PersistWF.UI.Activities; using System; using System.Activities; using System.Activities.Statements; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace PersistWF.UI { public sealed class UserWF : Activity { public InArgument<string> Name { get; set; } public InArgument<string> Email { get; set; } public InArgument<string> Phone { get; set; } public InArgument<string> ConnectionString { get; set; } public InArgument<TextWriter> Writer { get; set; } public UserWF() { Variable<User> User = new Variable<User> { Name = "User" }; this.Implementation = () => new Sequence { DisplayName = "EnterUser", Variables = { User }, Activities = { new CreateUser // 1. ایجاد کاربر با ورود پارامترهای ورودی { ConnectionString = new InArgument<string>(c=> ConnectionString.Get(c)), Email = new InArgument<string>(c=> Email.Get(c)), Name = new InArgument<string>(c=> Name.Get(c)), Phone = new InArgument<string>(c=> Phone.Get(c)), User = new OutArgument<User>(c=> User.Get(c)) }, new WriteLine // 2. لاگ مربوط به دخیره کاربر { TextWriter = new InArgument<TextWriter>(c=> Writer.Get(c)), Text = new InArgument<string>(c=> string.Format("User {0} Registered and waiting for Accept", Name.Get(c) ) ) }, new InvokeMethod { TargetType = typeof(ApplicationInterface), // 3. برای به روزرسانی لیست کاربران ثبت شده در نمایش فرم MethodName = "NewUser", Parameters = { new InArgument<User>(env => User.Get(env)) } }, new WaitForAccept<User> // 4. اینجا فرایند متوقف میشود و منتظر تایید مدیر میماند { BookmarkName = "GetAcceptes", Input = new OutArgument<User>(env => User.Get(env)) }, new WriteLine // 5. لاگ مربوط به تایید شدن کاربر { TextWriter = new InArgument<TextWriter>(c=> Writer.Get(c)), Text = new InArgument<string>(c=> string.Format("User {0} Accepter by {1}",Name.Get(c),User.Get(c).AcceptedBy)) } } }; } } }
اگر بخوایم از Designer استفاده کنیم. فرایندمان چیزی شبیه شکل زیر خواهد بود
به Application بر میگردیم تا آن را پیاده سازی کنیم. ابتدا به app.config که اتوماتیک ایجاد شده رفته تا اسم Connectionstring رو به UserGenerator تغییر دهیم. محتویات درون app.config به شکل زیر است.
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> </configSections> <connectionStrings> <add name="UserGenerator" connectionString="Data Source=.;Initial Catalog=PersistWF;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> </configuration>
در کلاس AddRequest کد زیر را اضافه میکنم. برای نگهداری مقدار connectionstring
private string _connectionString = "";
همچنین کدهای زیر را به رویداد Load فرم اضافه میکنم تا مقدار ConnectionString را از Config بخوانم:
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); ConnectionStringsSection css = (ConnectionStringsSection)config.GetSection("connectionStrings"); _connectionString = css.ConnectionStrings["UserGenerator"].ConnectionString;
خط زیر را هم به کلاس AddRequest اضافه نمایید.
private InstanceStore _instanceStore;
این ارجاعیه به کلاس InstanceStore که برای Persist و Load کردن workflow از آن استفاده میکنیم و کدهای زیر را هم به رویداد Load فرم اضافه میکنیم.
_instanceStore = new SqlWorkflowInstanceStore(_connectionString); InstanceView view = _instanceStore.Execute(_instanceStore.CreateInstanceHandle(), new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30)); _instanceStore.DefaultInstanceOwner = view.InstanceOwner;
InstanceStore یک کلاس abstract می باشد که همهی Providerهای مربوط به persistence از آن مشتق میشوند. در این پروژه من از کلاس SqlWorkflowInstanceStore استفاده کردم تا workflowها را در دیتابیسSQL Server ذخیره کنم.
برای ایجاد یک Request مقادیر را از فرم دریافت کرده، یک User ایجاد میکنیم و آن را در فرآیند به جریان میاندازیم. این کار را در رویداد کلیک دکمه Register انجام میدهیم
private void brnRegister_Click(object sender, RoutedEventArgs e) { Dictionary<string, object> parameters = new Dictionary<string, object>(); parameters.Add("Name", txtName.Text); parameters.Add("Phone", txtPhone.Text); parameters.Add("Email", txtEmail.Text); parameters.Add("ConnectionString", _connectionString); parameters.Add("Writer", new ListBoxTextWriter(lstEvents)); WorkflowApplication i = new WorkflowApplication (new UserWF(), parameters); // Setup persistence i.InstanceStore = _instanceStore; i.PersistableIdle = (waiea) => PersistableIdleAction.Unload; i.Run(); }
پارامترهای ورودی را از روی فرم مقدار دهی میکنیم. یک شی از کلاس WorkflowApplication ایجاد میکنیم. خاصیت InstanceStore آن را با Store ای که ایجاد کردیم مقدار دهی میکنیم. توسط رویداد PersistableIdle فرآیند رو مجبور میکنیم به Persist شدن و Unload شدن.
و سپس فرایند را اجرا میکنم.
اگر یادتان باشد، در فرآیند، از یک InvoceMethod استفاده کردیم. متد مورد نظر را هم در کلاس ApplicationInterface.cs ایجاد میکنیم.
public static void NewUser(User l) { if (_app != null) _app.AddNewUser(l); }
همین طور که میبینید، یک متد هم در کلاس AddRequest ایجاد میشود؛ با این محتوا
public void AddNewUser(User l) { this.lstUsers.Dispatcher.BeginInvoke(new Action(() => this.lstUsers.Items.Add(l))); }
این متد فقط یک کاربر را به لیست کاربران اضافه میکند. این لیست همه کاربران را نشان میدهد. توسط رویداد SelectionChanged این کنترل، کاربر انتخاب شده را بررسی کرده در صورتی که کاربر جدید باشد، امکان تایید شدن را برایش فراهم میکنیم؛ که نمایش دکمه تایید است.
private void lstUsers_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (lstUsers.SelectedIndex >= 0) { User l = (User)lstUsers.Items[lstUsers.SelectedIndex]; lblSelectedNotes.Visibility = Visibility.Visible; if (l.Status == "New") { lblAgent.Visibility = Visibility.Visible; txtAcceptedBy.Visibility = Visibility.Visible; btnAccept.Visibility = Visibility.Visible; } else { lblAgent.Visibility = Visibility.Hidden; txtAcceptedBy.Visibility = Visibility.Hidden; btnAccept.Visibility = Visibility.Hidden; } } else { lblSelectedNotes.Content = ""; lblSelectedNotes.Visibility = Visibility.Hidden; lblAgent.Visibility = Visibility.Hidden; txtAcceptedBy.Visibility = Visibility.Hidden; btnAccept.Visibility = Visibility.Hidden; } }
و برای رویداد کلیک دکمه تایید کاربر :
private void btnAccept_Click(object sender, RoutedEventArgs e) { if (lstUsers.SelectedIndex >= 0) { User u = (User)lstUsers.Items[lstUsers.SelectedIndex]; Guid id = u.WorkflowID.Value; UserDataDataContext dc = new UserDataDataContext(_connectionString); dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Users); u = dc.Users.SingleOrDefault<User>(x => x.WorkflowID == id); if (u != null) { u.AcceptedBy = txtAcceptedBy.Text; u.Status = "Assigned"; dc.SubmitChanges(); // Clear the input txtAcceptedBy.Text = ""; } // Update the grid lstUsers.Items[lstUsers.SelectedIndex] = u; lstUsers.Items.Refresh(); WorkflowApplication i = new WorkflowApplication(new UserWF()); i.InstanceStore = _instanceStore; i.PersistableIdle = (waiea) => PersistableIdleAction.Unload; i.Load(id); try { i.ResumeBookmark("GetAcceptes", u); } catch (Exception e2) { AddEvent(e2.Message); } } }
کاربر را انتخاب میکنم مقادیرش را تنظیم میکنیم. آن را دخیره کرده و workflow را از روی guid مربوط به آن که قبلا در فرآیند به Entity دادیم، Load میکنیم و همانطور که میبینید توسط متد ResumeBookmark فرآیند رو از جایی که میخواهیم ادامه میدهیم. البته میتوان تایید کاربر را هم در خود فرآیند انجام داد و چون نوشتن Activity مرتبط با آن تقریبا تکراری است با اجازهی شما من اون رو ننوشتم و زحمتش با خودتونه.
حالا فقط ماندهاست که همه کاربران را در ابتدای نمایش فرم از دیتابیس فراخوانی کنیم و در لیست نمایش دهیم:
private void LoadExistingLeads() { UserDataDataContext dc = new UserDataDataContext(_connectionString); dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Users); IEnumerable<User> q = dc.Users; foreach (User u in q) { AddNewUser(u); } }
و فراخوانی این متد را به انتهای رویداد Load صفحه واگذار میکنیم.
پروژه رو اجرا کرده و یک کاربر را اضافه میکنم. همانطور که میدانید این کاربر در فرآیند ایجاد و در دیتابیس ذخیره میشود
برنامه را میبندم و دوباره اجرا میکنم. کاربر را انتخاب میکنم و یک نام برای admin انتخاب و آن را تایید میکنم. فرآیند را از bookmark مورد نظر اجرا کرده و به پایان میرسد. با بسته شدن برنامه، فرایند Idle و Unload میشود و ذخیره آن در sqlserver صورت میگیرد.