معرفی DNTProfiler
هدیهی نوروزی سایت net tips. پروژهی پروفایلر سورس بازی است که با EF 6.x و همچنین NHibernate 4.x سازگار است. این پروژه از دو قسمت کلاینت و سرور تشکیل میشود.
نصب کلاینت EF برنامهی DNTProfiler
تفاوتی نمیکند که برنامهی شما وبی است یا ویندوزی؛ برای هر دو حالت ابتدا دستور ذیل را در کنسول پاورشل نیوگت اجرا کنید:
PM> Install-Package DNTProfiler.EntityFramework.Core
<configuration> <entityFramework> <interceptors> <interceptor type="DNTProfiler.EntityFramework.Core.DatabaseLogger, DNTProfiler.EntityFramework.Core"> <parameters> <parameter value="http://localhost:8080" /> <parameter value="|DataDirectory|\ErrorsLog.Log" /> </parameters> </interceptor> </interceptors> </entityFramework> </configuration>
دریافت و راه اندازی برنامهی DNTProfiler
آخرین نگارش برنامهی DNTProfiler را از برگهی releases مخزن کد آن میتوانید دریافت کنید:
https://github.com/VahidN/DNTProfiler/releases
این برنامه برای دات نت 4 نوشته شدهاست. بنابراین اگر هنوز از ویندوز XP استفاده میکنید، امکان کار کردن با آنرا خواهید داشت.
البته بستهی نیوگت DNTProfiler.EntityFramework.Core آن برای دات نت 4 و 4.5 تهیه شدهاست و به صورت خودکار بر اساس ساختار پروژهی شما، یکی از آنها نصب خواهد شد.
تا اینجا کار راه اندازی این برنامه به پایان میرسد. برای استفادهی از آن باید ابتدا برنامهی DNTProfiler را اجرا کنید. این برنامه به پیامهای رسیدهی از برنامهی اصلی شما (ارسال شده توسط DNTProfiler.EntityFramework.Core) گوش فرا میدهد و سپس شروع به آنالیز آنها خواهد کرد. ساختار این تبادل اطلاعات هم بر اساس تهیهی یک ASP.NET Self host Web API است.
این برنامه به صورت ماژولار تهیه شدهاست و تمام آنالیز کنندههای آن در حقیقت یک پلاگین هستند (در حال حاضر دارای 32 پلاگین است). این پلاگینها در گروههای Alerts (برای مثال یافتن جوینهای تکراری و یک سری موارد بهینه سازی سرعت)، Loggers (طبقه بندی خام اطلاعات رسیده)، Visualizers (نمایش بصری اطلاعات رسیده) قرار میگیرند.
نظرات، پیشنهادات و همکاری
لطفا برای طرح سؤالات و ارائهی پیشنهادات خود در زمینهی این پروژه، به قسمت اختصاصی آن در سایت مراجعه نمائید:
https://www.dntips.ir/projects/details/21
استفاده از NuGet Package Manager برای مدیریت وابستگیها
درباره اهمیت NuGet برای مصرف کنندگان قبلا این مطلب نوشته شده است.
بجای صرف وقت برای اینکه بررسی کنیم آیا این نسخهی جدید کتابخانهی X یا اسکریپت jQuery آمده است یا خیر، میتوان این وظیفه را به NuGet سپرد. علاوه بر این NuGet مزیتهای دیگری هم دارد؛ مثلا تیمهای برنامه نویسی میتوانند کتاب خانههای مشترک خودشان را در مخزنهای سفارشی NuGet قرار دهند و توزیع و Versioning آنرا به NuGet بسپارند.
تکیه بر Abstraction (انتزاع)
Abstraction در طراحی سیستمها منجر به تولید نرم افزار هایی Loosely coupled با قابلیت نگهداری بالا و همچنین فراهم شدن زمینه برای نوشتن Unit Test میشود.
اگر به مطالب قبلی وب سایت برگردیم در مطلب چرا ASP.NET MVC گفته شد که :
2) دستیابی به کنترل بیشتر بر روی اجزای فریم ورک :
در طراحی ASP.NET MVC همهجا interfaceها قابل مشاهد هستند. همین مساله به معنای افزونه پذیری اکثر قطعات تشکیل دهنده ASP.NET MVC است؛ برخلاف ASP.NET web forms. برای مثال تابحال چندین view engine، routing engine و غیره توسط برنامه نویسهای مستقل برای ASP.NET MVC طراحی شدهاند که هیچکدام با ASP.NET web forms میسر نیست. برای مثال از view engine پیش فرض آن خوشتان نمیآید؟ عوضش کنید! سیستم اعتبار سنجی توکار آنرا دوست ندارید؟ آنرا با یک نمونه بهتر تعویض کنید و الی آخر ...
به علاوه طراحی بر اساس interfaceها یک مزیت دیگر را هم به همراه دارد و آن هم ساده سازی mocking (تقلید) آنها است جهت ساده سازی نوشتن آزمونهای واحد.
از کلمهی کلیدی New استفاده نکنید
هر جا ممکن است کار وهله سازی اشیاء را به لایه و حتی Framework دیگری بسپارید. هر زمان اشیاء نرم افزار خودتان را با کلمهی new وهله سازی میکنید اصل Abstraction را فراموش کرده اید. هر زمان اشیاء نرم افزار را مستقیم وهله سازی میکنید در نظر داشته باشید میتوانید آنها را به صورت وابستگی تزریق کنید.
در همین رابطه مطالب زیر را از دست ندهید :
- تزریق وابستگی (dependency injection) به زبان ساده
- تزریق وابستگی (Dependency Injection) و توسعه پذیری
از HttpContext مستقیما استفاده نکنید (از HttpContextBase استفاده کنید)
از .NET 4 به بعد فضای نامی تعریف شده که در بر دارندهی کلاسهای انتزاعی (Abstraction) خیلی از قسمتهای اصلی ASP.NET است. یکی از مواردی که در توسعهی ASP.NET معمولا زیاد استفاده میشود، شیء HttpContext است . استفاده از HttpContextBase را به استفاده از HttpContext ترجیح دهید. اهمیت این موضوع در راستای اهمیت انتزاع (Abstraction) میباشد.
از "رشتههای جادویی" اجتناب کنید
استفاده از رشتههای جادویی در خیلی از جاها کار را ساده میکند؛ بعضی وقتها هم به آنها نیاز است اما مشکلات زیادی دارند :
- رشتهها معنای باطنی ندارند (مثلا : دشوار است که از روی نام یک ID مشخص کنم این ID چطور به ID دیگری مرتبط است و یا اصلا ربط دارد یا خیر)
- با اشتباهات املایی یا عدم رعایت حروف بزرگ و کوچک ایجاد مشکل میکنند.
- به Refactoring واکنش خوبی نشان نمیدهند. (برای درک بهتر این مطلب را بخوانید.)
<p> <label for="FirstName">First Name:</label> <span id="FirstName">@ViewData["FirstName"]</span> </p>
<p> <label for="FirstName">First Name:</label> <span id="FirstName">@Model.FirstName</span> </p>
در رابطه با Magic strings این مطلب را مطالعه بفرمایید.
از نوشتن HTML در کدهای "Backend" خودداری کنید
با توجه به اصل جداسازی وابستگیها (Separation of Concerns) وظیفهی کنترلرها و دیگر کدهای backend رندر کردن HTML نیست. (ساده سازی کنترلر ها) البته در نظر داشته باشید که قطعا تولید HTML در متدهای کمکی کلاس هایی که "تنها" وظیفهی آنها کمک به Viewها جهت تولید کد هست ایرادی ندارد. این کلاسها بخشی از View در نظر گرفته میشوند نه کدهای "backend".
در Viewها "Business logic" انجام ندهید
معکوس بند قبلی هم کاملا صدق میکند ، منظور این است که Viewها تا جایی که ممکن است باید حاوی کمترین Business logic ممکن باشند. در واقع تمرکز Viewها باید استفاده و نحوهی نمایش داده ای که برای آنها فراهم شده باشد نه انجام عملیات روی آن.
استفاده از Presentation Model را به استفاده مستقیم از Business Objectها ترجیح دهید
در مطالب مختلف وب سایت اشاره به اهمین ViewModelها شده است. برای اطلاعات بیشتر بند ج آموزش 11 از سری آموزشهای ASP.NET MVC را مطالعه بفرمایید.
Ifهای شرطی را در Viewها را در متدهای کمکی کپسوله کنید
استفاده از شرطها در View کار توسعه دهنده را برای یک سری اعمال ساده میکند اما میتواند باعث کمی کثیف کاری هم شود. مثلا:
@if(Model.IsAnonymousUser) { <img src="@Url.Content("~/content/images/anonymous.jpg")" /> } else if(Model.IsAdministrator) { <img src="@Url.Content("~/content/images/administrator.jpg")" /> } else if(Model.Membership == Membership.Standard) { <img src="@Url.Content("~/content/images/member.jpg")" /> } else if(Model.Membership == Membership.Preferred) { <img src="@Url.Content("~/content/images/preferred_member.jpg")" /> }
public static string UserAvatar(this HtmlHelper<User> helper) { var user = helper.ViewData.Model; string avatarFilename = "anonymous.jpg"; if (user.IsAnonymousUser) { avatarFilename = "anonymous.jpg"; } else if (user.IsAdministrator) { avatarFilename = "administrator.jpg"; } else if (user.Membership == Membership.Standard) { avatarFilename = "member.jpg"; } else if (user.Membership == Membership.Preferred) { avatarFilename = "preferred_member.jpg"; } var urlHelper = new UrlHelper(helper.ViewContext.RequestContext); var contentPath = string.Format("~/content/images/{0}", avatarFilename) string imageUrl = urlHelper.Content(contentPath); return string.Format("<img src='{0}' />", imageUrl); }
@Html.UserAvatar()
منتظر قسمت بعدی باشید.
همچنین Issue tracker آنها هم مدتی است که به آدرس جدیدی منتقل شده است: [^]
و ... اگر علاقمند باشید که از آخرین تغییرات این کتابخانه آگاه شوید، زیاد به دنبال وبلاگ یا سایت خاصی نگردید. روش متداول کار با کتابخانههای سورس باز، دنبال کردن change log ارسالی آنها به سیستمهای سورس کنترل است (همان متنی که حین commit ارسال میکنند). برای مثال جهت آگاه شدن از آخرین تغییرات NHibernate مشترک این فید شوید: [^]
در خیلی از مواقع 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 صورت میگیرد.
public IActionResult Create([FromBody] CreateViewModel vm) {
[ModelBinder(BinderType = typeof(PersianDateModelBinder))] public partial class CreateViewModel { public int Id { get; set; } [DisplayName("عنوان مطلب")] public string Title { get; set; } [DisplayName("نگارش مطلب")] public string Content { get; set; } [DisplayName("تاریخ درج")] public DateTime CreateDate { get; set; } = DateTime.Now.Date; [DisplayName("تاریخ انتشار")] public DateTime PublishDate { get; set; } = DateTime.Now.Date; }
قسمتی از یک پروژه به همراه کلاس SqlHelper آن در کامنتهای مطلب «اهمیت Code review» توسط یکی از خوانندگان بلاگ جهت Code review مطرح شده که بهتر است در یک مطلب جدید و مجزا به آن پرداخته شود. قسمت مهم آن کلاس SqlHelper است و مابقی در اینجا ندید گرفته میشوند:
//It's only for code review purpose!
using System.Data;
using System.Data.SqlClient;
using System.Web.Configuration;
public sealed class SqlHelper
{
private SqlHelper() { }
// Send Connection String
//---------------------------------------------------------------------------------------
public static string GetCntString()
{
return WebConfigurationManager.ConnectionStrings["db_ConnectionString"].ConnectionString;
}
// Connect to Data Base SqlServer
//---------------------------------------------------------------------------------------
public static SqlConnection Connect2Db(ref SqlConnection sqlCnt, string cntString)
{
try
{
if (sqlCnt == null) sqlCnt = new SqlConnection();
sqlCnt.ConnectionString = cntString;
if (sqlCnt.State != ConnectionState.Open) sqlCnt.Open();
return sqlCnt;
}
catch (SqlException)
{
return null;
}
}
// Run ExecuteScalar Command
//---------------------------------------------------------------------------------------
public static string RunExecuteScalarCmd(ref SqlConnection sqlCnt, string strCmd, bool blnClose)
{
Connect2Db(ref sqlCnt, GetCntString());
using (sqlCnt)
{
using(SqlCommand sqlCmd = sqlCnt.CreateCommand())
{
sqlCmd.CommandText = strCmd;
object objResult = sqlCmd.ExecuteScalar();
if (blnClose) CloseCnt(ref sqlCnt, true);
return (objResult == null) ? string.Empty : objResult.ToString();
}
}
}
// Close SqlServer Connection
//---------------------------------------------------------------------------------------
public static bool CloseCnt(ref SqlConnection sqlCnt, bool nullSqlCnt)
{
try
{
if (sqlCnt == null) return true;
if (sqlCnt.State == ConnectionState.Open)
{
sqlCnt.Close();
sqlCnt.Dispose();
}
if (nullSqlCnt) sqlCnt = null;
return true;
}
catch (SqlException)
{
return false;
}
}
}
مثالی از نحوه استفاده ارائه شده:
protected void BtnTest_Click(object sender, EventArgs e)
{
SqlConnection sqlCnt = new SqlConnection();
string strQuery = "SELECT COUNT(UnitPrice) AS PriceCount FROM [Order Details]";
// در این مرحله پارامتر سوم یعنی کانکشن باز نگه داشته شود
string strResult = SqlHelper.RunExecuteScalarCmd(ref sqlCnt, strQuery, false);
strQuery = "SELECT LastName + N'-' + FirstName AS FullName FROM Employees WHERE (EmployeeID = 9)";
// در این مرحله پارامتر سوم یعنی کانکشن بسته شود
strResult = SqlHelper.RunExecuteScalarCmd(ref sqlCnt, strQuery, true);
}
مروری بر این کد:
1) نحوه کامنت نوشتن
بین سی شارپ و زبان سی++ تفاوت وجود دارد. این نحوه کامنت نویسی بیشتر در سی++ متداول است. اگر از ویژوال استودیو استفاده میکنید، مکان نما را به سطر قبل از یک متد منتقل کرده و سه بار پشت سر هم forward slash را تایپ کنید. به صورت خودکار ساختار خالی زیر تشکیل خواهد شد:
/// <summary>
///
/// </summary>
/// <param name="sqlCnt"></param>
/// <param name="cntString"></param>
/// <returns></returns>
public static SqlConnection Connect2Db(ref SqlConnection sqlCnt, string cntString)
این روش مرسوم کامنت نویسی کدهای سی شارپ است. خصوصا اینکه ابزارهایی وجود دارند که به صورت خودکار از این نوع کامنتها، فایل CHM درست میکنند.
2) وجود سازنده private
احتمالا هدف این بوده که نه شخصی و نه حتی کامپایلر، وهلهای از این کلاس را ایجاد نکند. بنابراین بهتر است کلاسی را که تمام متدهای آن static است (که به این هم خواهیم رسید!) ، راسا static معرفی کنید. به این ترتیب نیازی به سازنده private نخواهد بود.
3) وجود try/catch
یک اصل کلی وجود دارد: اگر در حال طراحی یک کتابخانه پایهای هستید، try/catch را در هیچ متدی از آن لحاظ نکنید. بله؛ درست خوندید! لطفا try/catch ننویسید! کرش کردن برنامه خوب است! لایههای بالاتر برنامه که در حال استفاده از کدهای شما هستند متوجه خواهند شد که مشکلی رخ داده و این مشکل توسط کتابخانه مورد استفاده «خفه» نشده. برای مثال اگر هم اکنون SQL Server در دسترس نیست، لایههای بالاتر برنامه باید این مشکل را متوجه شوند. Exception اصلا چیز بدی نیست! کرش برنامه اصلا بد نیست!
فرض کنید که دچار بیماری شدهاید. اگر مثلا تبی رخ ندهد، از کجا باید متوجه شد که نیاز به مراقبت پزشکی وجود دارد؟ اگر هیچ علامتی بروز داده نشود که تا الان نسل بشر منقرض شده بود!
4) وجود ref و out
دوستان گرامی! این ref و out فقط جهت سازگاری با زبان C در سی شارپ وجود دارد. لطفا تا حد ممکن از آن استفاده نکنید! مثلا استفاده از توابع API ویندوز که با C نوشته شدهاند.
یکی از مهمترین کاربردهای pointers در زبان سی، دریافت بیش از یک خروجی از یک تابع است. برای مثال یک متد API ویندوز را فراخوانی میکنید؛ خروجی آن یک ساختار است که به کمک pointers به عنوان یکی از پارامترهای همان متد معرفی شده. این روش به وفور در طراحی ویندوز بکار رفته. ولی خوب در سی شارپ که از این نوع مشکلات وجود ندارد. یک کلاس ساده را طراحی کنید که چندین خاصیت دارد. هر کدام از این خاصیتها میتوانند نمایانگر یک خروجی باشند. خروجی متد را از نوع این کلاس تعریف کنید. یا برای مثال در دات نت 4، امکان دیگری به نام Tuples معرفی شده برای کسانی که سریع میخواهند چند خروجی از یک تابع دریافت کنند و نمیخواهند برای اینکار یک کلاس بنویسند.
ضمن اینکه برای مثال در متد Connect2Db، هم کانکشن یکبار به صورت ref معرفی شده و یکبار به صورت خروجی متد. اصلا نیازی به استفاده از ref در اینجا نبوده. حتی نیازی به خروجی کانکشن هم در این متد وجود نداشته. کلیه تغییرات شما در شیء کانکشنی که به عنوان پارامتر ارسال شده، در خارج از آن متد هم منعکس میشود (شبیه به همان بحث pointers در زبان سی). بنابراین وجود ref غیرضروری است؛ وجود خروجی متد هم به همین صورت.
5) استفاده از using در متد RunExecuteScalarCmd
استفاده از using خیلی خوب است؛ همیشه اینکار را انجام دهید!
اما اگر اینکار را انجام دادید، بدانید که شیء sqlCnt در پایان بدنه using ، توسط GC نابوده شده است. بنابراین اینجا bool blnClose دیگر چه کاربردی دارد؟! تصمیم شما دیگر اهمیتی نخواهد داشت؛ چون کار تخریبی پیشتر انجام شده.
6) متد CloseCnt
این متد زاید است؛ به دلیلی که در قسمت (5) عنوان شد. using های استفاده شده، کار را تمام کردهاند. بنابراین بستن اشیاء dispose شده معنا نخواهد داشت.
7) در مورد نحوه استفاده
اگر SqlHelper را در اینجا مثلا یک DAL ساده فرض کنیم (data access layer)، جای قسمت BLL (business logic layer) در اینجا خالی است. عموما هم چون توضیحات این موارد را خیلی بد ارائه دادهاند، افراد از شنیدن اسم آنها هم وحشت میکنند. BLL یعنی کمی دست به Refactoring بزنید و این پیاده سازی منطق تجاری ارائه شده در متد BtnTest_Click را به یک کلاس مجزا خارج از code behind پروژه منتقل کنید. Code behind فقط محل استفاده نهایی از آن باشد. همین! فعلا با همین مختصر شروع کنید.
مورد دیگری که در اینجا باز هم مشهود است، عدم استفاده از پارامتر در کوئریها است. چون از پارامتر استفاده نکردهاید، SQL Server مجبور است برای حالت EmployeeID = 9 یکبار execution plan را محاسبه کند، برای کوئری بعدی مثلا EmployeeID = 19، اینکار را تکرار کند و الی آخر. این یعنی مصرف حافظه بالا و همچنین سرعت پایین انجام کوئریها. بنابراین اینقدر در قید و بند باز نگه داشتن یک کانکشن نباشید؛ مشکل اصلی جای دیگری است!
8) برنامه وب و اطلاعات استاتیک!
این پروژه، یک پروژه ASP.NET است. دیدن تعاریف استاتیک در این نوع پروژهها یک علامت خطر است! در این مورد قبلا مطلب نوشتم:
متغیرهای استاتیک و برنامههای ASP.NET
یک درخواست عمومی!
لطف کنید در پروژهای «جدید» خودتون این نوع کلاسهای SqlHelper رو «دور بریزید». یاد گرفتن کار با یک ORM جدید اصلا سخت نیست. مثلا طراحی Entity framework مایکروسافت به حدی ساده است که هر شخصی با داشتن بهره هوشی در حد یک عنکبوت آبی یا حتی جلبک دریایی هم میتونه با اون کار کنه! فقط NHibernate هست که کمی مرد افکن است و گرنه مابقی به عمد ساده طراحی شدهاند.
مزایای کار کردن با ORM ها این است:
- کوئریهای حاصل از آنها «پارامتری» است؛ که این دو مزیت عمده را به همراه دارد:
امنیت: مقاومت در برابر SQL Injection
سرعت و همچنین مصرف حافظه کمتر: با کوئریهای پارامتری در SQL Server همانند رویههای ذخیره شده رفتار میشود.
- عدم نیاز به نوشتن DAL شخصی پر از باگ. چون ORM یعنی همان DAL که توسط یک سری حرفهای طراحی شده.
- یک دست شدن کدها در یک تیم. چون همه بر اساس یک اینترفیس مشخص کار خواهند کرد.
- امکان استفاده از امکانات جدید زبانهای دات نتی مانند LINQ و نوشتن کوئریهای strongly typed تحت کنترل کامپایلر.
- پایین آوردن هزینههای آموزشی افراد در یک تیم. مثلا EF را میشود به عنوان یک پیشنیاز در نظر گرفت؛ عمومی است و همه گیر. کسی هم از شنیدن نام آن تعجب نخواهد کرد. کتاب(های) آموزشی هم در مورد آن زیاد هست.
و ...
قسمت قبل بیشتر آشنایی با یک سری از اصطلاحات مرتبط با فریم ورک MAF بود و همچنین نحوهی کلی استفاده از آن. در این قسمت یک مثال ساده را با آن پیاده سازی خواهیم کرد و فرض قسمت دوم بر این است که افزونهی Visual Studio Pipeline Builder را نیز نصب کردهاید.
یک نکته پیش از شروع:
- اگر افزونهی Visual Studio Pipeline Builder پس از نصب به منوی Tools اضافه نشده است، یک پوشهی جدید را به نام Addins در مسیر Documents\Visual Studio 2008 ایجاد کرده و سپس فایلهای آنرا در این مسیر کپی کنید.
ساختار اولیه یک پروژه MAF
- پروژههایی که از MAF استفاده میکنند، نیاز به ارجاعاتی به دو اسمبلی استاندارد System.AddIn.dll و System.AddIn.Contract.dll دارند (مطابق شکل زیر):
- ساختار آغازین یک پروژه MAF از سه پروژه تشکیل میشود که توسط افزونهی Visual Studio Pipeline Builder به 7 پروژه بسط خواهد یافت.
این سه پروژه استاندارد آغازین شامل موارد زیر هستند:
- هاست: همان برنامهی اصلی که قرار است از افزونه استفاده کند.
- قرار داد: نحوهی تعامل هاست و افزونه در این پروژه تعریف میشود. (یک پروژه از نوع class library)
- افزونه: کار پیاده سازی قرار داد را عهده دار خواهد شد. (یک پروژه از نوع class library)
- همچنین مرسوم است جهت مدیریت بهتر خروجیهای حاصل شده یک پوشه Output را نیز به این solution اضافه کنند:
اکنون با توجه به این محل خروجی، به خواص Build سه پروژه موجود مراجعه کرده و مسیر Build را اندکی اصلاح خواهیم کرد (هر سه مورد بهتر است اصلاح شوند)، برای مثال:
نکتهی مهم هم اینجا است که خروجی host باید به ریشه این پوشه تنظیم شود و سایر پروژهها هر کدام خروجی خاص خود را در پوشهای داخل این ریشه باید ایجاد کنند.
تا اینجا قالب اصلی کار آماده شده است. قرارداد ما هم به شکل زیر است (ویژگی AddInContract آن نیز نباید فراموش شود):
using System.AddIn.Pipeline;
using System.AddIn.Contract;
namespace CalculatorConract
{
[AddInContract]
public interface ICalculatorContract : IContract
{
double Operate(string operation, double a, double b);
}
}
به عبارت دیگر برنامهای محاسباتی داریم (هاست) که دو عدد double را در اختیار افزونههای خودش قرار میدهد و سپس این افزونهها یک عملیات ریاضی را بر روی آنها انجام داده و خروجی را بر میگردانند. نوع عملیات توسط آرگومان operation مشخص میشود. این آرگومان به کلیه افزونههای موجود ارسال خواهد شد و احتمالا یکی از آنها این مورد را پیاده سازی کرده است. در غیر اینصورت یک استثنای عملیات پیاده سازی نشده صادر میشود.
البته روش بهتر طراحی این افزونه، اضافه کردن متد یا خاصیتی جهت مشخص کردن نوع و یا انواع عملیات پشتیبانی شده توسط افزونه است که جهت سادگی این مثال، به این طراحی ساده اکتفا میشود.
ایجاد pipeline
اگر قسمت قبل را مطالعه کرده باشید، یک راه حل مبتنی بر MAF از 7 پروژه تشکیل میشود که عمدهترین خاصیت آنها مقاوم کردن سیستم در مقابل تغییرات نگارش قرارداد است. در این حالت اگر قرار داد تغییر کند، نه هاست و نه افزونهی قدیمی، نیازی به تغییر در کدهای خود نخواهند داشت و این پروژههای میانی هستند که کار وفق دادن (adapters) نهایی را برعهده میگیرند.
برای ایجاد خودکار View ها و همچنین Adapters ، از افزونهی Visual Studio Pipeline Builder که پیشتر معرفی شد استفاده خواهیم کرد.
سه گزینهی آن هم مشخص هستند. نام پروژهی قرارداد، مسیر پروژهی هاست و مسیر خروجی نهایی معرفی شده. پیش از استفاده از این افزونه نیاز است تا یکبار solution مورد نظر کامپایل شود. پس از کلیک بر روی دکمهی OK، پروژههای ذکر شده ایجاد خواهند شد:
پس از ایجاد این پروژهها، نیاز به اصلاحات مختصری در مورد نام اسمبلی و فضای نام هر کدام میباشد؛ زیرا به صورت پیش فرض هر کدام به نام template نامگذاری شدهاند:
پیاده سازی افزونه
قالب کاری استفاده از این فریم ورک آماده است. اکنون نوبت به پیاده سازی یک افزونه میباشد. به پروژه AddIn مراجعه کرده و ارجاعی را به اسمبلی AddInView خواهیم افزود. به این صورت افزونهی ما به صورت مستقیم با قرارداد سروکار نداشته و ارتباطات، در راستای همان pipeline تعریف شده، جهت مقاوم شدن در برابر تغییرات صورت میگیرد:
using System;
using CalculatorConract.AddInViews;
using System.AddIn;
namespace CalculatorAddIn
{
[AddIn]
public class MyCalculatorAddIn : ICalculator
{
public double Operate(string operation, double a, double b)
{
throw new NotImplementedException();
}
}
}
در اینجا افزونهی ما باید اینترفیس ICalculator مربوط به AddInView را پیاده سازی نماید که برای مثال خواهیم داشت:
using System;
using CalculatorConract.AddInViews;
using System.AddIn;
namespace CalculatorAddIn
{
[AddIn("افزونه یک", Description = "توضیحات", Publisher = "نویسنده", Version = "نگارش یک")]
public class MyCalculatorAddIn : ICalculator
{
public double Operate(string operation, double a, double b)
{
switch (operation)
{
case "+":
return a + b;
case "-":
return a - b;
case "*":
return a * b;
default:
throw new NotSupportedException("عملیات مورد نظر توسط این افزونه پشتیبانی نمیشود");
}
}
}
}
همانطور که در قسمت قبل نیز ذکر شد، این کلاس باید با ویژگی AddIn مزین شود که توسط آن میتوان توضیحاتی در مورد نام ، نویسنده و نگارش افزونه ارائه داد.
استفاده از افزونهی تولید شده
هاست برای استفاده از افزونههایی با قرارداد ذکر شده، مطابق pipeline پروژه، نیاز به ارجاعی به اسمبلی HostView دارد و در اینجا نیز هاست به صورت مستقیم با قرارداد کاری نخواهد داشت. همچنین هاست هیچ ارجاع مستقیمی به افزونهها نداشته و بارگذاری و مدیریت آنها به صورت پویا انجام خواهد شد.
نکتهی مهم!
در هر دو ارجاع به HostView و یا AddInView باید خاصیت Copy to local به false تنظیم شود، در غیر اینصورت افزونهی شما بارگذاری نخواهد شد.
پس از افزودن ارجاعی به HostView، نمونهای از استفاده از افزونهی تولید شده به صورت زیر میتواند باشد که توضیحات مربوطه به صورت کامنت آورده شده است:
using System;
using System.AddIn.Hosting;
using CalculatorConract.HostViews;
namespace Calculator
{
class Program
{
private static ICalculator _calculator;
static void doOperation()
{
Console.WriteLine("1+2: {0}", _calculator.Operate("+", 1, 2));
}
static void Main(string[] args)
{
//مسیر پوشه ریشه مربوطه به خط لوله افزونهها
string path = Environment.CurrentDirectory;
//مشخص سازی مسیر خواندن و کش کردن افزونهها
AddInStore.Update(path);
//یافتن افزونههایی سازگار با شرایط قرارداد پروژه
//در اینجا هیچ افزونهای بارگذاری نمیشود
var addIns = AddInStore.FindAddIns(typeof(ICalculator), path);
//اگر افزونهای یافت شد
if (addIns.Count > 0)
{
var addIn = addIns[0]; //استفاده از اولین افزونه
Console.WriteLine("1st addIn: {0}", addIn.Name);
//فعال سازی افزونه و همچنین مشخص سازی سطح دسترسی آن
_calculator = addIn.Activate<ICalculator>(AddInSecurityLevel.Intranet);
//یک نمونه از استفاده آن
doOperation();
}
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
چند نکته جالب توجه در مورد قابلیتهای ارائه شده:
- مدیریت load و unload پویا
- امکان تعریف سطح دسترسی و ویژگیهای امنیتی اجرای یک افزونه
- امکان ایزوله سازی پروسه اجرای افزونه از هاست (در ادامه توضیح داده خواهد شد)
- مقاوم بودن پروژه به نگارشهای مختلف قرارداد
اجرای افزونه در یک پروسه مجزا
حتما با امکانات مرورگر کروم و یا IE8 در مورد اجرای هر tab آنها در یک پروسهی مجزا از پروسه اصلی هاست مطلع هستید. به این صورت پروسهی هاست از رفتار tab ها محافظت میشود، همچنین پروسهی هر tab نیز از tab دیگر ایزوله خواهد بود. یک چنین قابلیتی در این فریم ورک نیز پیش بینی شده است.
//فعال سازی افزونه و همچنین مشخص سازی سطح دسترسی آن
//همچنین جدا سازی پروسه اجرایی افزونه از هاست
_calculator = addIn.Activate<ICalculator>(
new AddInProcess(),
AddInSecurityLevel.Intranet);
در این حالت اگر پس از فعال شدن افزونه، یک break point قرار دهیم و به task manager ویندوز مراجعه نمائیم، پروسهی مجزای افزونه قابل مشاهده است.
برای مطالعه بیشتر + ، + ، + و +
در یک طبقه بندی کلی، عملگرهای پرس و جو بر اساس ورودی و خروجی آنها به سه دسته تقسیم میشوند:
1- نتیجهی توالی ورودی، بصورت یک توالی، به خروجی ارسال میشود.
2- نتیجهی توالی ورودی، بصورت یک عنصر یکتا و واحد به خروجی ارسال میشود.
3- اثری از ورودی در توالی خروجی وجود ندارد (این عملگرها عناصر خودشان را تولید میکنند).
دستهی آخر شاید کمی عجیب به نطر برسد. این عملگرها هیچ توالی ورودی را دریافت نمیکنند. مثلا میتوان از طریق این عملگرها، یک توالی از اعداد صحیح را تولید کرد.
تقسیم بندی عملگرهای پرس و جو بر اساس عملکرد به صورت زیر میباشد :
- محدود کننده (Restriction)
where
- بازتابی (Projection)
Select,SelectMany
- جداکننده (Partitioning)
Take,Skip,TakeWhile,SkipWhile
- مرتب سازی (Ordering)
OrderBy,OrderByDescending,ThenBy,ThenByDescending,Reverse
- گروه بندی (Grouping)
GroupBy
- مجموعه (Set)
Concat,Union,Intersect,Except
- تبدیل (Conversion)
ToArray,ToList,ToDictionary,ToLookup,OfType,Cast
- عنصر(Element)
First,FirstOrDefault,Last,LastOrDefalt,Single,SingleOrDefault
- عنصر در (ElementAt)
ElementAtOrDefault,DefaultIfEmpty
- تولید (Generation)
Empty,Range,Report
- کمی (Quantifier)
Any,All,Contains,SequenceEqual
- مجموعه (Aggregate)
Count,LongCount,Sum,Min,Max,Average,Aggregate
- اتصال (Join)
Join,GroupJoin,Zip
در این مطلب عملگرهای محدود کننده، بازتابی و جداکننده، بررسی خواهند شد. بعد از معرفی هر عملگر، معادل عبارتهای پرس و جوی آنها نیز معرفی خواهند شد.
این عملگر، عناصری را به خروجی ارسال میکند که با گزارهی (Predicate) تعریف شده مطابقت داشته باشند.
نکته : گزاره (Predicate) تابعی است که اگر شرط آن تامین شود، مقدار true و در غیر اینصورت مقدار false را باز میگرداند.
مثال :
Ingredient[] ingredients = { new Ingredient{Name = "Sugar", Calories=500}, new Ingredient{Name = "Egg", Calories=100}, new Ingredient{Name = "Milk", Calories=150}, new Ingredient{Name = "Flour", Calories=50}, new Ingredient{Name = "Butter", Calories=200}, }; IEnumerable<Ingredient> query = ingredients.Where(x => x.Calories >= 200); foreach (var ingredient in query) { Console.WriteLine(ingredient.Name); }
خروجی کد بالا:
Sugar Butter
IEnumerable<Ingredient> query = ingredients.Where((ingredient, index) => ingredient.Name == "Sugar" || index == 4);
پیاده سازی توسط عبارتهای پرس و جو
در روش عبارتهای پرس و جو، کلمهی کلیدی where بههمراه یک عبارت منطقی در پرس و جو ظاهر میشود:
IEnumerable<Ingredient> gueryExpression = from i in ingredients where i.Calories >= 200 select i;
عملگرهای بازتاب (Projection Operators)
عملگرهای پرس و جوی بازتابی، یک توالی ورودی را دریافت و با تبدیل عناصر آنها، یک توالی خروجی را تولید میکنند.
Select
عملگر پرس و جوی select هر عنصر توالی ورودی را به یک عنصر در توالی خروجی تبدیل میکند. تعداد عناصر ورودی و خروجی در این حالت یکسان میباشند.
پرس و جوی زیر عناصر توالی ورودی Ingredient را به عناصر رشتهای در توالی خروجی بازتاب میکند. عبارت Lambda تعریف شده، نحوهی بازتاب عناصر را مشخص میکند (هر عنصر ingredient به یک عنصر رشتهای بازتاب میشود):
IEnumerable<string> query = ingredients.Select(x => x.Name);
IEnumerable<int> query = ingredients.Select(x => x.Name.Length);
در عملیات بازتاب میتوان یک شیء جدید را در توالی خروجی ایجاد کرد. در کد زیر عناصر Ingredient به یک عنصر جدید از نوع IngredientNameAndLenght بازتاب شده است.
class IngredientNameAndLength { public string Name { get; set; } public int Length { get; set; } public override string ToString() { return Name + " " + Length; } } IEnumerable<IngredientNameAndLength> query = ingredients.Select(x => new IngredientNameAndLength { Name = x.Name, Length = x.Name.Length });
var query = ingredients.Select(x => new { Name = x.Name, Length = x.Name.Length });
{ Name = Sugar, Length = 5 } { Name = Egg, Length = 3 } { Name = Milk, Length = 4 } { Name = Flour, Length = 5 } { Name = Butter, Length = 6 }
پیاده سازی توسط عبارتهای پرس و جو
کلمهی کلیدی select در عبارتهای پرس و جو، به شکل زیر استفاده میشود:
var query = from i in ingredients select new { Name=i.Name, Length=i.Name.Length };
برعکس دستور select که به ازای هر عنصر در توالی ورودی، یک عنصر را در توالی خروجی بازتاب میکرد، دستور SelectMany ممکن است تعداد عناصر کمتر و یا بیشتری را در توالی خروجی بازتاب کند (انتخاب مقادیر یک مجموعه از مجموعهی دیگر).
عبارت Lambda نوشته شده در عملگر Select، یک مقدار را باز میگرداند. اما عبارت Lambda نوشته شده در عملگر SelectMany، یک توالی فرزند (Child Sequence) را ایجاد میکند. توالی فرزند ممکن است حاوی تعداد مختلفی از عناصر به ازای هر عنصر در توالی ورودی باشد.
در مثال زیر عبارت Lambda یک توالی فرزند از کاراکترها ایجاد میکند (یک کاراکتر به ازای هر حرف از هر عنصر توالی ورودی). بهطور مثال عنصر ورودی Sugar، پس از پردازش توسط عبارت Lambda، یک توالی فرزند با 5 عنصر 's','u','g','e','r' فراهم میکند. هر رشتهی در توالی Ingredient میتواند تعداد حروف متفاوتی داشته باشد. در نتیجه عبارت Lambda، توالیهای فرزندی با طولهای مختلف ایجاد میکند.
مثال:
string[] ingredients = {"Sugar","Egg","Milk","Flour","Butter"}; IEnumerable<char> query = ingredients.SelectMany(x => x.ToCharArray()); foreach (var item in query) { Console.WriteLine(item); }
S u g a r E g g M i l k F l o u r B u t t e r
پیاده سازی توسط عبارتهای پرس و جو
در روش عبارتهای پرس و جو یک عبارت (clause) اضافی from برای تولید یک توالی فرزند به کار برده میشود. خروجی کد زیر مشابه کد قبلی است:
string[] ingredients = {"Sugar","Egg","Milk","Flour","Butter"}; IEnumerable<char> query2 = from i in ingredients from c in i.ToCharArray() select c; foreach (var item in query2) { Console.WriteLine(item); }
عملگرهای جداکننده، یک توالی ورودی را دریافت و آنها را از هم جدا میکنند.
Take
عملگر Takeیک توالی ورودی را دریافت کرده و تعداد مشخصی از توالی را باز میگرداند.
مثال: عملگر Take، سه عضو اول توالی Ingredient را باز میگرداند:
Ingredient[] ingredients = { new Ingredient{Name = "Sugar", Calories=500}, new Ingredient{Name = "Egg", Calories=100}, new Ingredient{Name = "Milk", Calories=150}, new Ingredient{Name = "Flour", Calories=50}, new Ingredient{Name = "Butter", Calories=200}, }; IEnumerable<Ingredient> query = ingredients.Take(3); foreach (var ingredient in query) { Console.WriteLine(ingredient.Name); }
Sugar Egg Milk
Ingredient[] ingredients = { new Ingredient{Name = "Sugar", Calories=500}, new Ingredient{Name = "Egg", Calories=100}, new Ingredient{Name = "Milk", Calories=150}, new Ingredient{Name = "Flour", Calories=50}, new Ingredient{Name = "Butter", Calories=200}, }; IEnumerable<Ingredient> query = ingredients.Where(x=>x.Calories>100).Take(2); foreach (var ingredient in query) { Console.WriteLine(ingredient.Name); }
Sugar Milk
پیاده سازی توسط عبارتهای پرس و جو
کلمهی کلیدی (Key word) جایگزینی برای عملگر Take وجود ندارد، ولی میتوان با ترکیب دو روش نوشتن پرس و جو، خروجی مورد نظر را تولید کرد:
IEnumerable<Ingredient> query = (from i in ingredients where i.Calories > 100 select i).Take(2); TakeWhile
کد زیر تا زمانی که خصوصیت Calorie توالی ورودی بزرگتر و مساوی 100 باشد، عناصر را جدا میکند.
Ingredient[] ingredients = { new Ingredient{Name = "Sugar", Calories=500}, new Ingredient{Name = "Egg", Calories=100}, new Ingredient{Name = "Milk", Calories=150}, new Ingredient{Name = "Flour", Calories=50}, new Ingredient{Name = "Butter", Calories=200}, }; IEnumerable<Ingredient> query = ingredients.TakeWhile(x => x.Calories >= 100); foreach (var ingredient in query) { Console.WriteLine(ingredient.Name); }
Sugar Egg Milk
پیاده سازی توسط عبارتهای پرس و جو
برای این عملگر هم کلمهی کلیدی (Key word) جایگزینی وجود ندارد و با ترکیب دو روش نوشتن پرس و جو نتیجهی دلخواه حاصل میشود.
Skip
این عملگر تعداد مشخصی از عناصر را از ابتدای توالی نادیده گرفته و باقی عناصر را باز میگرداند.
کد زیر سه عضو اول توالی را نادیده گرفته و مابقی را باز میگرداند:
Ingredient[] ingredients = { new Ingredient{Name = "Sugar", Calories=500}, new Ingredient{Name = "Egg", Calories=100}, new Ingredient{Name = "Milk", Calories=150}, new Ingredient{Name = "Flour", Calories=50}, new Ingredient{Name = "Butter", Calories=200}, }; IEnumerable<Ingredient> query = ingredients.Skip(3); foreach (var ingredient in query) { Console.WriteLine(ingredient.Name); }
Flour Butter
پیاده سازی توسط عبارتهای پرس و جو
برای این عملگر هم کلمهی کلیدی (Key word) جایگزینی وجود ندارد و با ترکیب دو روش نوشتن پرس و جو، نتیجهی دلخواه حاصل میشود.
با ترکیب عملگر Take و Skip میتوان اطلاعات را بهصورت صفحه بندی به کاربر ارائه کرد. مثال زیر این حالت را نشان میدهد.
IEnumerable<Ingredient> firstPage = ingredients.Take(2); IEnumerable<Ingredient> secondPage = ingredients.Skip(2).Take(2); IEnumerable<Ingredient> thirdPage = ingredients.Skip(4).Take(2); Console.WriteLine("First Page : "); foreach (var ingredient in firstPage) { Console.WriteLine(" - " + ingredient.Name); } Console.WriteLine("Second Page : "); foreach (var ingredient in secondPage) { Console.WriteLine(" - " + ingredient.Name); } Console.WriteLine("Third Page : "); foreach (var ingredient in thirdPage) { Console.WriteLine(" - " + ingredient.Name); }
First Page : - Sugar - Egg Second Page : - Milk - Flour Third Page : - Butter SkipWhile
مثال:
Ingredient[] ingredients = { new Ingredient{Name = "Sugar", Calories=500}, new Ingredient{Name = "Egg", Calories=100}, new Ingredient{Name = "Milk", Calories=150}, new Ingredient{Name = "Flour", Calories=50}, new Ingredient{Name = "Butter", Calories=200}, }; IEnumerable<Ingredient> query = ingredients.SkipWhile(x => x.Name != "Milk"); foreach (var ingredient in query) { Console.WriteLine(ingredient.Name); }
Milk Flour Butter
[RegularExpression(@"^[\u0600-\u06FF,\u0590-\u05FF,0-9\s]*$", ErrorMessage = "لطفا تنها از اعداد و حروف فارسی استفاده نمائید")] public string FriendlyName { get; set; }