برای مثال، جدول Post مدل بلاگ را در نظر بگیرید که در آن دو فیلد Body و Image تعریف شده اند.فیلد Body از نوع nvarchar max و فیلد Image از نوع varbinary max است و بدیهی است که این دو داده، به هنگام واکشی حافظهی زیادی مصرف میکنند.موارد بسیاری وجود دارند که ما به اطلاعات این دو فیلد احتیاجی نداریم از جمله: نمایش پستهای پر بازدید، پسته هایی که اخیرا ارسال شده اند و اصولا ما فقط به چند فیلد جدول Post احتیاج داریم و نه همهی آن ها.
namespace SplittingTableSample.DomainClasses { public class Post { public virtual int Id { get; set; } public virtual string Title { get; set; } public virtual DateTime CreatedDate { get; set; } public virtual string Body { get; set; } public virtual byte[] Image { get; set; } } }
دلیل اینکه در مدل فوق، تمامی خواص به صورت virtual تعریف شده اند، فعال سازی پروکسیهای ردیابی تغییر است. اگر دستور زیر را برای واکشی اطلاعات post با id=1 انجام دهیم:
using (var context = new MyDbContext()) { var post = context.Posts.Find(1); }
exec sp_executesql N'SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Title] AS [Title], [Extent1].[CreatedDate] AS [CreatedDate], [Extent1].[Body] AS [Body], [Extent1].[Image] AS [Image] FROM [dbo].[Posts] AS [Extent1] WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=1
همان طور که مشاهده میکنید، با اجرای دستور فوق تمامی فیلدهای جدول Posts که id آنها برابر 1 بود واکشی شدند، ولی من تنها به فیلدهای Id و Title آن احتیاج داشتم. خب شاید بگویید که من به سادگی با projection، این مشکل را حل میکنم و تنها از فیلد هایی که به آنها احتیاج دارم، کوئری میگیرم. همهی اینها درست، اما projection هم مشکلات خود را دارد،به صورت پیش فرض، نوع بدون نام بر میگرداند و اگر بخواهیم این گونه نباشد، باید مقادیر آن را به یک کلاس(مثلا viewmodel) نگاشت کنیم و کلی مشکل دیگر.
راه حل دیگری که برای حل این مشکل ارائه میشود و برای نرمال سازی جداول نیز کاربرد دارد این است که، جدول Posts را به دو جدول مجزا که با یکدیگر رابطهی یک به یک دارند تقسیم کنیم، فیلدهای پر مصرف را در یک جدول و فیلدهای حجیم و کم مصرف را در جدول دیگری تعریف کنیم و سپس یک رابطهی یک به یک بین آن دو برقرار میکنیم.
به طور مثال این کار را بر روی جدول Posts ، به شکل زیر انجام شده است:
namespace SplittingTableSample.DomainClasses { public class Post { public virtual int Id { get; set; } public virtual string Title { get; set; } public virtual DateTime CreatedDate { get; set; } public virtual PostMetaData PostMetaData { get; set; } } } namespace SplittingTableSample.DomainClasses { public class PostMetaData { public virtual int PostId { get; set; } public virtual string Body { get; set; } public virtual byte[] Image { get; set; } public virtual Post Post { get; set; } } }
namespace SplittingTableSample.DomainClasses { public class PostMetaDataConfig : EntityTypeConfiguration<PostMetaData> { public PostMetaDataConfig() { HasKey(x => x.PostId); HasRequired(x => x.Post).WithRequiredDependent(x => x.PostMetaData); } } }
var post = context.Posts.Find(1);
exec sp_executesql N'SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Title] AS [Title], [Extent1].[CreatedDate] AS [CreatedDate] FROM [dbo].[Posts] AS [Extent1] WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=1
اما راه حل ذکر شده نیز کاملا بدون ایراد نیست. مشکل اساسی آن تعدد تعداد جداول آن است. آیا جدول Post ، واقعا احتیاج به چنین سطح نرمال سازی و تبدیل آن به دو جدول مجزا را داشت؟ مطمئنا خیر. آیا واقعا راه حلی وجود دارد که ما در سمت کدهای خود با دو موجودیت مجزا کار کنیم، در صورتی که در دیتابیس این دو موجودیت، ساختار یک جدول را تشکیل دهند. در اینجا روشی مطرح میشود به نام تقسیم جدول (Table Splitting).
برای انجام این کار فقط چند تنظیم ساده لازم است:
1) فیلدهای موجودیت مورد نظر را به موجودیتهای کوچکتر، نگاشت میکنیم.
2) بین موجودیتهای کوچک تر، رابطهی یک به یک که هر دو سر رابطه Required هستند، رابطه برقرار میکنم.
3) با استفاده از Fluent API یا DataAnnotations، تمامی موجودیتها را به یک نام در دیتابیس نگاشت میکنیم.
برای مثال، تنظیمات Fluent برای کلاس Post و PostMetaData که رابطهی بین آنها یک به یک است را مشاهده میکنید:
namespace SplittingTableSample.DomainClasses { public class PostConfig : EntityTypeConfiguration<Post> { public PostConfig() { ToTable("Posts"); } } } namespace SplittingTableSample.DomainClasses { public class PostMetaDataConfig : EntityTypeConfiguration<PostMetaData> { public PostMetaDataConfig() { ToTable("Posts"); HasKey(x => x.PostId); HasRequired(x => x.Post).WithRequiredDependent(x => x.PostMetaData); } } }
اگر دستورات زیر را اجرا کنید:
var post = context.Posts.Find(1); Console.WriteLine(post.PostMetaData.Body);
برای متد Find خروجی زیر:
exec sp_executesql N'SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Title] AS [Title], [Extent1].[CreatedDate] AS [CreatedDate] FROM [dbo].[Posts] AS [Extent1] WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=1
exec sp_executesql N'SELECT [Extent1].[Id] AS [Id], [Extent1].[Body] AS [Body], [Extent1].[Image] AS [Image] FROM [dbo].[Posts] AS [Extent1] WHERE [Extent1].[Id] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1
حال اگر بخواهیم با یک رفت و آمد به دیتابیس کلیه اطلاعات را واکشی کنیم، میتوانیم از Eager Loading استفاده کنیم:
var post = context.Posts.Include(x => x.PostMetaData).SingleOrDefault(x => x.Id == 1);
SELECT [Limit1].[Id] AS [Id], [Limit1].[Title] AS [Title], [Limit1].[CreatedDate] AS [CreatedDate], [Extent2].[Id] AS [Id1], [Extent2].[Body] AS [Body], [Extent2].[Image] AS [Image] FROM (SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Title] AS [Title], [Extent1].[CreatedDate] AS [CreatedDate] FROM [dbo].[Posts] AS [Extent1] WHERE 1 = [Extent1].[Id] ) AS [Limit1] LEFT OUTER JOIN [dbo].[Posts] AS [Extent2] ON [Limit1].[Id] = [Extent2].[Id]
دریافت کدهای این بخش: SplittingTable-Sample.rar
<script src="@Url.Content("~/Scripts/Site.js")"></script>
<script src="~/Scripts/Site.js"></script>
<div @{if (myClass != null) { <text>class="@myClass"</text> } }>Content</div>
<div class="@myClass">Content</div>
<div>Content</div>
<input checked="@ViewBag.Checked" type="checkbox"/>
<input type="checkbox"/>
- آشنایی کامل با جاوااسکریپت
- آشنایی با Object-oriented در جاوااسکریپت
اولین قدم برای استفاده از Vue.js، نصب آن میباشد که مرحله به مرحله توضیح خواهم داد. جهت نصب، سایت Vue.js به ما چندین راه پیشنهاد میدهد که ما آنها را در ذیل بررسی میکنیم:
https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.0/vue.js
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.0/vue.js"></script>
$ npm install vue
3- CLI
$ npm install -g vue-cli $ vue init webpack my-project $ cd my-project $ npm install $ npm run dev
<html> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.0/vue.js"></script> </body> </html>
در خیلی از مواقع 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 صورت میگیرد.
روشهای زیادی برای پیاده سازی این نوع جستجوها وجود دارد. در این مقاله سعی شده گامهای ایجاد یک ساختار پایه برای این نوع فرمها و یک ایجاد فرم نمونه بر پایه ساختار ایجاد شده را با استفاده از یکی از همین روشها شرح دهیم.
اساس این روش تولید عبارت Linq بصورت پویا با توجه به انتخابهای کاربرمی باشد.
1- برای شروع یک سلوشن خالی با نام DynamicSearch ایجاد میکنیم. سپس ساختار این سلوشن را بصورت زیر شکل میدهیم.
در این مثال پیاده سازی در قالب ساختار MVVM در نظر گرفته شده. ولی محدودتی از این نظر برای این روش قائل نیستیم.
2- کار را از پروژه مدل آغاز میکنیم. جایی که ما برای سادگی کار، 3 کلاس بسیار ساده را به ترتیب زیر ایجاد میکنیم:
namespace DynamicSearch.Model { public class Person { public Person(string name, string family, string fatherName) { Name = name; Family = family; FatherName = fatherName; } public string Name { get; set; } public string Family { get; set; } public string FatherName { get; set; } } } using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DynamicSearch.Model { public class Teacher : Person { public Teacher(int id, string name, string family, string fatherName) : base(name, family, fatherName) { ID = id; } public int ID { get; set; } public override string ToString() { return string.Format("Name: {0}, Family: {1}", Name, Family); } } } namespace DynamicSearch.Model { public class Student : Person { public Student(int stdId, Teacher teacher, string name, string family, string fatherName) : base(name, family, fatherName) { StdID = stdId; Teacher = teacher; } public int StdID { get; set; } public Teacher Teacher { get; set; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using DynamicSearch.Model; namespace DynamicSearch.Service { public class StudentService { public IList<Student> GetStudents() { return new List<Student> { new Student(1,new Teacher(1,"Ali","Rajabi","Reza"),"Mohammad","Hoeyni","Sadegh"), new Student(2,new Teacher(2,"Hasan","Noori","Mohsen"),"Omid","Razavi","Ahmad"), }; } } }
جهت ساخت عبارت، نیاز به سه نوع جزء داریم:
-اتصال دهنده عبارات ( "و" ، "یا")
-عملوند (در اینجا فیلدی که قصد مقایسه با عبارت مورد جستجوی کاربر را داریم)
-عملگر ("<" ، ">" ، "=" ، ....)
برای ذخیره المانهای انتخاب شده توسط کاربر، سه کلاس زیر را ایجاد میکنیم (همان سه جزء بالا):
using System; using System.Linq.Expressions; namespace DynamicSearch.ViewModel.Base { public class AndOr { public AndOr(string name, string title,Func<Expression,Expression,Expression> func) { Title = title; Func = func; Name = name; } public string Title { get; set; } public Func<Expression, Expression, Expression> Func { get; set; } public string Name { get; set; } } } using System; namespace DynamicSearch.ViewModel.Base { public class Feild : IEquatable<Feild> { public Feild(string title, Type type, string name) { Title = title; Type = type; Name = name; } public Type Type { get; set; } public string Name { get; set; } public string Title { get; set; } public bool Equals(Feild other) { return other.Title == Title; } } } using System; using System.Linq.Expressions; namespace DynamicSearch.ViewModel.Base { public class Operator { public enum TypesToApply { String, Numeric, Both } public Operator(string title, Func<Expression, Expression, Expression> func, TypesToApply typeToApply) { Title = title; Func = func; TypeToApply = typeToApply; } public string Title { get; set; } public Func<Expression, Expression, Expression> Func { get; set; } public TypesToApply TypeToApply { get; set; } } }
using System.Collections.ObjectModel; using System.Linq; using System.Linq.Expressions; namespace DynamicSearch.ViewModel.Base { public abstract class SearchFilterBase<T> : BaseViewModel { protected SearchFilterBase() { var containOp = new Operator("شامل باشد", (expression, expression1) => Expression.Call(expression, typeof(string).GetMethod("Contains"), expression1), Operator.TypesToApply.String); var notContainOp = new Operator("شامل نباشد", (expression, expression1) => { var contain = Expression.Call(expression, typeof(string).GetMethod("Contains"), expression1); return Expression.Not(contain); }, Operator.TypesToApply.String); var equalOp = new Operator("=", Expression.Equal, Operator.TypesToApply.Both); var notEqualOp = new Operator("<>", Expression.NotEqual, Operator.TypesToApply.Both); var lessThanOp = new Operator("<", Expression.LessThan, Operator.TypesToApply.Numeric); var greaterThanOp = new Operator(">", Expression.GreaterThan, Operator.TypesToApply.Numeric); var lessThanOrEqual = new Operator("<=", Expression.LessThanOrEqual, Operator.TypesToApply.Numeric); var greaterThanOrEqual = new Operator(">=", Expression.GreaterThanOrEqual, Operator.TypesToApply.Numeric); Operators = new ObservableCollection<Operator> { equalOp, notEqualOp, containOp, notContainOp, lessThanOp, greaterThanOp, lessThanOrEqual, greaterThanOrEqual, }; SelectedAndOr = AndOrs.FirstOrDefault(a => a.Name == "Suppress"); SelectedFeild = Feilds.FirstOrDefault(); SelectedOperator = Operators.FirstOrDefault(a => a.Title == "="); } public abstract IQueryable<T> GetQuarable(); public virtual ObservableCollection<AndOr> AndOrs { get { return new ObservableCollection<AndOr> { new AndOr("And","و", Expression.AndAlso), new AndOr("Or","یا",Expression.OrElse), new AndOr("Suppress","نادیده",(expression, expression1) => expression), }; } } public virtual ObservableCollection<Operator> Operators { get { return _operators; } set { _operators = value; NotifyPropertyChanged("Operators"); } } public abstract ObservableCollection<Feild> Feilds { get; } public bool IsOtherFilters { get { return _isOtherFilters; } set { _isOtherFilters = value; } } public string SearchValue { get { return _searchValue; } set { _searchValue = value; NotifyPropertyChanged("SearchValue"); } } public AndOr SelectedAndOr { get { return _selectedAndOr; } set { _selectedAndOr = value; NotifyPropertyChanged("SelectedAndOr"); NotifyPropertyChanged("SelectedFeildHasSetted"); } } public Operator SelectedOperator { get { return _selectedOperator; } set { _selectedOperator = value; NotifyPropertyChanged("SelectedOperator"); } } public Feild SelectedFeild { get { return _selectedFeild; } set { Operators = value.Type == typeof(string) ? new ObservableCollection<Operator>(Operators.Where(a => a.TypeToApply == Operator.TypesToApply.Both || a.TypeToApply == Operator.TypesToApply.String)) : new ObservableCollection<Operator>(Operators.Where(a => a.TypeToApply == Operator.TypesToApply.Both || a.TypeToApply == Operator.TypesToApply.Numeric)); if (SelectedOperator == null) { SelectedOperator = Operators.FirstOrDefault(a => a.Title == "="); } NotifyPropertyChanged("SelectedOperator"); NotifyPropertyChanged("SelectedFeild"); _selectedFeild = value; NotifyPropertyChanged("SelectedFeildHasSetted"); } } public bool SelectedFeildHasSetted { get { return SelectedFeild != null && (SelectedAndOr.Name != "Suppress" || !IsOtherFilters); } } private ObservableCollection<Operator> _operators; private Feild _selectedFeild; private Operator _selectedOperator; private AndOr _selectedAndOr; private string _searchValue; private bool _isOtherFilters = true; } }
در گام بعد، یک کلاس کمکی برای سهولت ساخت عبارات ایجاد میکنیم:
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using AutoMapper; namespace DynamicSearch.ViewModel.Base { public static class ExpressionExtensions { public static List<T> CreateQuery<T>(Expression whereCallExpression, IQueryable entities) { return entities.Provider.CreateQuery<T>(whereCallExpression).ToList(); } public static MethodCallExpression CreateWhereCall<T>(Expression condition, ParameterExpression pe, IQueryable entities) { var whereCallExpression = Expression.Call( typeof(Queryable), "Where", new[] { entities.ElementType }, entities.Expression, Expression.Lambda<Func<T, bool>>(condition, new[] { pe })); return whereCallExpression; } public static void CreateLeftAndRightExpression<T>(string propertyName, Type type, string searchValue, ParameterExpression pe, out Expression left, out Expression right) { var typeOfNullable = type; typeOfNullable = typeOfNullable.IsNullableType() ? typeOfNullable.GetTypeOfNullable() : typeOfNullable; left = null; var typeMethodInfos = typeOfNullable.GetMethods(); var parseMethodInfo = typeMethodInfos.FirstOrDefault(a => a.Name == "Parse" && a.GetParameters().Count() == 1); var propertyInfos = typeof(T).GetProperties(); if (propertyName.Contains(".")) { left = CreateComplexTypeExpression(propertyName, propertyInfos, pe); } else { var propertyInfo = propertyInfos.FirstOrDefault(a => a.Name == propertyName); if (propertyInfo != null) left = Expression.Property(pe, propertyInfo); } if (left != null) left = Expression.Convert(left, typeOfNullable); if (parseMethodInfo != null) { var invoke = parseMethodInfo.Invoke(searchValue, new object[] { searchValue }); right = Expression.Constant(invoke, typeOfNullable); } else { //type is string right = Expression.Constant(searchValue.ToLower()); var methods = typeof(string).GetMethods(); var firstOrDefault = methods.FirstOrDefault(a => a.Name == "ToLower" && !a.GetParameters().Any()); if (firstOrDefault != null) left = Expression.Call(left, firstOrDefault); } } public static Expression CreateComplexTypeExpression(string searchFilter, IEnumerable<PropertyInfo> propertyInfos, Expression pe) { Expression ex = null; var infos = searchFilter.Split('.'); var enumerable = propertyInfos.ToList(); for (var index = 0; index < infos.Length - 1; index++) { var propertyInfo = infos[index]; var nextPropertyInfo = infos[index + 1]; if (propertyInfos == null) continue; var propertyInfo2 = enumerable.FirstOrDefault(a => a.Name == propertyInfo); if (propertyInfo2 == null) continue; var val = Expression.Property(pe, propertyInfo2); var propertyInfos3 = propertyInfo2.PropertyType.GetProperties(); var propertyInfo3 = propertyInfos3.FirstOrDefault(a => a.Name == nextPropertyInfo); if (propertyInfo3 != null) ex = Expression.Property(val, propertyInfo3); } return ex; } public static Expression AddOperatorExpression(Func<Expression, Expression, Expression> func, Expression left, Expression right) { return func.Invoke(left, right); } public static Expression JoinExpressions(bool isFirst, Func<Expression, Expression, Expression> func, Expression expression, Expression ex) { if (!isFirst) { return func.Invoke(expression, ex); } expression = ex; return expression; } } }
using System.Collections.ObjectModel; using System.Linq; using DynamicSearch.Model; using DynamicSearch.Service; using DynamicSearch.ViewModel.Base; namespace DynamicSearch.ViewModel { public class StudentSearchFilter : SearchFilterBase<Student> { public override ObservableCollection<Feild> Feilds { get { return new ObservableCollection<Feild> { new Feild("نام دانش آموز",typeof(string),"Name"), new Feild("نام خانوادگی دانش آموز",typeof(string),"Family"), new Feild("نام خانوادگی معلم",typeof(string),"Teacher.Name"), new Feild("شماره دانش آموزی",typeof(int),"StdID"), }; } } public override IQueryable<Student> GetQuarable() { return new StudentService().GetStudents().AsQueryable(); } } }
در نهایت زمل فایل موجود در پروژه ویو:
<Window x:Class="DynamicSearch.View.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:viewModel="clr-namespace:DynamicSearch.ViewModel;assembly=DynamicSearch.ViewModel" xmlns:view="clr-namespace:DynamicSearch.View" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> <Window.Resources> <viewModel:StudentSearchViewModel x:Key="StudentSearchViewModel" /> <view:VisibilityConverter x:Key="VisibilityConverter" /> </Window.Resources> <Grid DataContext="{StaticResource StudentSearchViewModel}"> <WrapPanel Orientation="Vertical"> <DataGrid AutoGenerateColumns="False" Name="asd" CanUserAddRows="False" ItemsSource="{Binding BindFilter}"> <DataGrid.Columns> <DataGridTemplateColumn> <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <ComboBox MinWidth="100" DisplayMemberPath="Title" ItemsSource="{Binding AndOrs}" Visibility="{Binding IsOtherFilters,Converter={StaticResource VisibilityConverter}}" SelectedItem="{Binding SelectedAndOr,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <DataGridTemplateColumn > <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <ComboBox IsEnabled="{Binding SelectedFeildHasSetted}" MinWidth="100" DisplayMemberPath="Title" ItemsSource="{Binding Feilds}" SelectedItem="{Binding SelectedFeild,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged }"/> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <DataGridTemplateColumn> <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <ComboBox MinWidth="100" DisplayMemberPath="Title" ItemsSource="{Binding Operators}" IsEnabled="{Binding SelectedFeildHasSetted}" SelectedItem="{Binding SelectedOperator,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <DataGridTemplateColumn Width="*"> <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <TextBox IsEnabled="{Binding SelectedFeildHasSetted}" MinWidth="200" Text="{Binding SearchValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/> <!--<TextBox Text="{Binding SearchValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>--> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid> <Button Content="+" HorizontalAlignment="Left" Command="{Binding AddFilter}"/> <Button Content="Result" Command="{Binding ExecuteSearchFilter}"/> <DataGrid ItemsSource="{Binding Results}"> </DataGrid> </WrapPanel> </Grid> </Window>
برخی منابع جهت آشنایی با Expression ها:
http://msdn.microsoft.com/en-us/library/bb882637.aspx
انتخاب پویای فیلدها در LINQ
http://www.persiadevelopers.com/articles/dynamiclinqquery.aspx
نکته: کدهای نوشته شده در این مقاله، نسخههای نخستین هستند و طبیعتا جا برای بهبود بسیار دارند. دوستان میتوانند در این امر به بنده کمک کنند.
پیشنهادات جهت بهبود:
- جداسازی کدهای پیاده کننده منطق از ویو مدلها جهت افزایش قابلیت نگهداری کد و سهولت استفاده در سایر ساختارها
- افزودن توضیحات به کد
- انتخاب نامگذاریهای مناسب تر
DynamicSearch.zip
.NET Core -> ASP.NET Core Web Application (.NET Core) -> Select `Empty` Template
در اینجا نقش Solution همانند نگارشهای قبلی ویژوال استودیو است: ظرفی است برای ساماندهی موارد مورد نیاز جهت تشکیل یک برنامهی وب و شامل مواردی است مانند پروژهها، تنظیمات آنها و غیره. بنابراین هنوز در اینجا فایل sln. تشکیل میشود.
نقش فایل global.json
زمانیکه یک پروژهی جدید ASP.NET Core 1.0 را آغاز میکنیم، ساختار پوشههای آن به صورت زیر هستند:
در اینجا هنوز فایل sln. قابل مشاهده است. همچنین در اینجا فایل جدیدی به نام global.json نیز وجود دارد، با این محتوا:
{ "projects": [ "src", "test" ], "sdk": { "version": "1.0.0-preview2-003121" } }
خاصیت projects در اینجا به صورت یک آرایه تعریف شدهاست و بیانگر محل واقع شدن پوشههای اصلی پروژهی جاری هستند. پوشهی src یا source را در تصویر فوق مشاهده میکنید و محلی است که سورسهای برنامه در آن قرار میگیرند. یک پوشهی test نیز در اینجا ذکر شدهاست و اگر در حین ایجاد پروژه، گزینهی ایجاد unit tests را هم انتخاب کرده باشید، این پوشهی مخصوص نیز ایجاد خواهد شد.
نکتهی مهم اینجا است، هرکدی که درون پوشههای ذکر شدهی در اینجا قرار نگیرد، قابلیت build را نخواهد داشت. به عبارتی این نسخهی از ASP.NET پوشهها را قسمتی از پروژه به حساب میآورد. در نگارشهای قبلی ASP.NET، مداخل تعریف فایلهای منتسب به هر پروژه، درون فایلی با پسوند csproj. قرار میگرفتند. معادل این فایل در اینجا اینبار پسوند xproj را دارد و اگر آنرا با یک ادیتور متنی باز کنید، فاقد تعاریف مداخل فایلهای پروژه است.
در این نگارش جدید اگر فایلی را به پوشهی src اضافه کنید یا حذف کنید، بلافاصله در solution explorer ظاهر و یا حذف خواهد شد.
یک آزمایش: به صورت معمول از طریق windows explorer به پوشهی src برنامه وارد شده و فایل پیش فرض Project_Readme.html را حذف کنید. سپس به solution explorer ویژوال استودیو دقت کنید. مشاهده خواهید کرد که این فایل، بلافاصله از آن حذف میشود. در ادامه به recycle bin ویندوز مراجعه کرده و این فایل حذف شده را restore کنید تا مجددا به پوشهی src برنامه اضافه شود. اینبار نیز افزوده شدن خودکار و بلافاصلهی این فایل را میتوان در solution explorer مشاهده کرد.
بنابراین ساختار مدیریت فایلهای این نگارش از ASP.NET در ویژوال استودیو، بسیار شبیه به ساختار مدیریت فایلهای VSCode شدهاست که آن نیز بر اساس پوشهها کار میکند و یک پوشه و تمام محتوای آنرا به صورت پیش فرض به عنوان یک پروژه میشناسد. به همین جهت دیگر فایل csproj ایی در اینجا وجود ندارد و file system همان project system است.
یک نکته: در اینجا مسیرهای مطلق را نیز میتوان ذکر کرد:
"projects": [ "src", "test", "c:\\sources\\Configuration\\src" ],
کامپایل خودکار پروژه در ASP.NET Core 1.0
علاوه بر تشخیص خودکار کم و زیاد شدن فایلهای سیستمی پروژه، بدون نیاز به Add new item کردن آنها در ویژوال استودیو، اگر سورسهای برنامه را نیز تغییر دهید، فایل سورس جدیدی را اضافه کنید و یا فایل سورس موجودی را حذف کنید، کل پروژه به صورت خودکار کامپایل میشود و نیازی نیست اینکار را به صورت دستی انجام دهید.
یک آزمایش: برنامه را از طریق منوی debug و گزینهی start without debugging اجرا کنید. اگر برنامه را در حالت معمول debug->start debugging اجرا کنید، حالت کامپایل خودکار را مشاهده نخواهید کرد. در اینجا (پس از start without debugging) یک چنین خروجی را مشاهده خواهید کرد:
این خروجی حاصل اجرای کدهای درون فایل Startup.cs برنامه است:
app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); });
await context.Response.WriteAsync("Hello DNT!");
این مساله قابلیت استفادهی از ASP.NET Core را در سایر ادیتورهای موجود، مانند VSCode سهولت میبخشد.
نقش فایل project.json
فایل جدید project.json مهمترین فایل تنظیمات یک پروژهی ASP.NET Core است و مهمترین قسمت آن، قسمت وابستگیهای آن است:
"dependencies": { "Microsoft.NETCore.App": { "version": "1.0.0", "type": "platform" }, "Microsoft.AspNetCore.Diagnostics": "1.0.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0" },
در نگارشهای قبلی ASP.NET، فایلی XML ایی به نام packages.config حاوی تعاریف مداخل بستههای نیوگت برنامه بود. این فایل در اینجا جزئی از محتوای فایل project.json در قسمت dependencies آن است.
با قسمت وابستگیهای این فایل، به دو طریق میتوان کار کرد:
الف) ویرایش مستقیم این فایل که به همراه intellisense نیز هست. با افزودن مداخل جدید به این فایل و ذخیره کردن آن، بلافاصله کار restore و دریافت و نصب آنها آغاز میشود:
ب) از طریق NuGet package manager
روش دیگر کار با وابستگیها، کلیک راست بر روی گره references و انتخاب گزینهی manage nuget packages است:
برای نمونه جهت نصب ASP.NET MVC 6 این مراحل باید طی شوند:
ابتدا برگهی browse را انتخاب کنید و سپس تیک مربوط به include prerelease را نیز انتخاب نمائید.
البته بستهی اصلی MVC در اینجا Microsoft.AspNetCore.Mvc نام دارد و نه MVC6.
اینبار بستههایی که restore میشوند، در مسیر اشتراکی C:\Users\user_name\.nuget\packages ذخیره خواهند شد.
یک نکتهی مهم:
قرار هست در نگارشهای پس از RTM، فایلهای project.json و xproj را جهت سازگاری با MSBuild، اندکی تغییر دهند (که این تغییرات به صورت خودکار توسط VS.NET انجام میشود). اطلاعات بیشتر
انتخاب فریم ورکهای مختلف در فایل project.json
در قسمت قبل عنوان شد که ASP.NET Core را میتوان هم برفراز NET Core. چندسکویی اجرا کرد و هم NET 4.6. مختص به ویندوز. این انتخابها در قسمت frameworks فایل project.json انجام میشوند:
"frameworks": { "netcoreapp1.0": { "imports": [ "dotnet5.6", "portable-net45+win8" ] } },
در اینجا اگر علاقمند بودید که از دات نت کامل مخصوص ویندوز نیز استفاده کنید، میتوانید آنرا در لیست فریم ورکها اضافه نمائید (در این مثال، دات نت کامل 4.5.2 نیز ذکر شدهاست):
"frameworks": { "netcoreapp1.0": { }, "net452": { } }
- “netcoreapp1.0” برای معرفی و استفادهی از NET Core 1.0. بکار میرود.
- جهت معرفی فریم ورکهای کامل و ویندوزی دات نت، اسامی “net45”, “net451”, “net452”, “net46”, “net461” مجاز هستند.
- “portable-net45+win8” برای معرفی پروفایلهای PCL یا portable class libraries بکار میرود.
- “dotnet5.6”, “dnxcore50” برای معرفی نگارشهای پیش نمایش NET Core.، پیش از ارائهی نگارش RTM استفاده میشوند.
- “netstandard1.2”, “netstandard1.5” کار معرفی برنامههای NET Standard Platform. را انجام میدهند.
بر این مبنا، dotnet5.6 ذکر شدهی در قسمت تنظیمات نگارش RTM، به این معنا است که قادر به استفادهی از بستههای نیوگت و کتابخانههای تولید شدهی با نگارشهای RC نیز خواهید بود (هرچند برنامه از netcoreapp1.0 استفاده میکند).
یک مثال: قسمت فریم ورکهای فایل project.json را به نحو ذیل جهت معرفی دات نت 4.6.1 تغییر دهید:
"frameworks": { "netcoreapp1.0": { "imports": [ "dotnet5.6", "portable-net45+win8" ] }, "net461": { "imports": [ "portable-net45+win8" ], "dependencies": { } } },
در این حالت پس از ذخیرهی فایل و شروع خودکار بازیابی وابستگیها، با پیام خطای Package Microsoft.NETCore.App 1.0.0 is not compatible with net461 متوقف خواهید شد.
برای رفع این مشکل باید وابستگی Microsoft.NETCore.App را حذف کنید، چون با net461 سازگاری ندارد
"dependencies": { //"Microsoft.NETCore.App": { // "version": "1.0.0", // "type": "platform" //},
فریم ورک دوم استفاده شده نیز در اینجا قابل مشاهده است.
فایل project.lock.json چیست؟
ذیل فایل project.json، فایل دیگری به نام project.lock.json نیز وجود دارد. اگر به محتوای آن دقت کنید، این فایل حاوی لیست دقیق بستههای نیوگت مورد استفادهی توسط برنامه است و الزاما با آنچیزی که در فایل project.json قید شده، یکی نیست. از این جهت که در فایل project.json، قید میشود netcoreapp1.0؛ ولی این netcoreapp1.0 دقیقا شامل چه بستههایی است؟ لیست کامل آنها را در این فایل میتوانید مشاهده کنید.
در ابتدای این فایل یک خاصیت locked نیز وجود دارد که مقدار پیش فرض آن false است. اگر به true تنظیم شود، در حین restore وابستگیهای برنامه، تنها از نگارشهای ذکر شدهی در این فایل استفاده میشود. از این جهت که در فایل project.json میتوان شماره نگارشها را با * نیز مشخص کرد؛ مثلا *.1.0.0
پوشهی جدید wwwroot و گره dependencies
یکی از پوشههای جدیدی که در ساختار پروژهی ASP.NET Core معرفی شدهاست، wwwroot نام دارد:
از دیدگاه هاستینگ برنامه، این پوشه، پوشهای است که در معرض دید عموم قرار میگیرد (وب روت). برای مثال فایلهای ایستای اسکریپت، CSS، تصاویر و غیره باید در این پوشه قرار گیرند تا توسط دنیای خارج قابل دسترسی و استفاده شوند. بنابراین سورس کدهای برنامه خارج از این پوشه قرار میگیرند.
گره dependencies که ذیل پوشهی wwwroot قرار گرفتهاست، جهت مدیریت این وابستگیهای سمت کلاینت برنامه است. در اینجا میتوان از NPM و یا Bower برای دریافت و به روز رسانی وابستگیهای اسکریپتی و شیوهنامههای برنامه کمک گرفت (علاوه بر نیوگت که بهتر است صرفا جهت دریافت وابستگیهای دات نتی استفاده شود).
یک مثال: فایل جدیدی را به نام bower.json به پروژهی جاری با این محتوا اضافه کنید:
{ "name": "asp.net", "private": true, "dependencies": { "bootstrap": "3.3.6", "jquery": "2.2.0", "jquery-validation": "1.14.0", "jquery-validation-unobtrusive": "3.2.6" } }
پس از اضافه شدن فایل bower.json، بلافاصله کار restore بستهها از اینترنت شروع میشود:
و یا با کلیک راست بر روی گره dependencies، گزینهی restore packages نیز وجود دارد.
فایلهای نهایی دریافت شده را در پوشهی bower_components خارج از wwwroot میتوانید مشاهده کنید.
در مورد نحوهی توزیع و دسترسی به فایلهای استاتیک یک برنامهی ASP.NET Core 1.0، نکات خاصی وجود دارند که در قسمتهای بعد، بررسی خواهند شد.
یک نکته: اگر خواستید نام پوشهی wwwroot را تغییر دهید، فایل جدیدی را به نام hosting.json با این محتوا به پروژه اضافه کنید:
{ "webroot":"AppWebRoot" }
نقطهی آغازین برنامه کجاست؟
اگر به فایل project.json دقت کنید، چنین تنظیمی در آن موجود است:
"buildOptions": { "emitEntryPoint": true, "preserveCompilationContext": true },
public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup<Startup>() .Build(); host.Run(); }
var outputKind = compilerOptions.EmitEntryPoint.GetValueOrDefault() ? OutputKind.ConsoleApplication : OutputKind.DynamicallyLinkedLibrary;
در فایل Program.cs تنظیماتی را مشاهده میکنید، در مورد راه اندازی Kestler که وب سرور بسیار سریع و چندسکویی کار با برنامههای ASP.NET Core 1.0 است و قسمت مهم دیگر آن به استفادهی از کلاس Startup بر میگردد (()<UseStartup<Startup). این کلاس را در فایل جدید Startup.cs میتوانید ملاحظه کنید که کار تنظیمات آغازین برنامه را انجام میدهد. اگر پیشتر با OWIN، در نگارشهای قبلی ASP.NET کار کرده باشید، قسمتی از این فایل برای شما آشنا است:
public class Startup { public void ConfigureServices(IServiceCollection services) { } public void Configure(IApplicationBuilder app) { app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } }
نقش فایل launchsetting.json
محتویات پیش فرض این فایل برای قالب empty پروژههای ASP.NET Core 1.0 به صورت ذیل است:
{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:7742/", "sslPort": 0 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Core1RtmEmptyTest": { "commandName": "Project", "launchBrowser": true, "launchUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
پروفایلهایی که در اینجا ذکر شدهاند، در تنظیمات پروژه نیز قابل مشاهده هستند: (کلیک راست بر روی پروژه و مشاهدهی properties آن و یا دوبار کلیک بر روی گره properties)
به علاوه امکان انتخاب این پروفایلها در زمان آغاز برنامه نیز وجود دارند:
نکتهی مهم تمام این موارد به قسمت environment variable قابل مشاهدهی در تصاویر فوق بر میگردد. این متغیر محیطی میتواند سه مقدار Development ، Staging و Production را داشته باشد و بر اساس این متغیر و مقدار آن، میتوان پروفایل جدیدی را تشکیل داد. زمانیکه برنامه بر اساس پروفایل خاصی بارگذاری میشود، اینکه چه متغیر محیطی انتخاب شدهاست، در کلاس Startup قابل استخراج و بررسی بوده و بر این اساس میتوان اقدامات خاصی را انجام داد. برای مثال تنظیمات خاصی را بارگذاری کرد و یا صفحات ویژهای را فعال و غیرفعال کرد (این موارد را در قسمتهای بعدی مرور میکنیم).
همچنین در اینجا به ازای هر پروفایل مختلف میتوان Url آغازین یا launch url و پورت آنرا مجزا درنظر گرفت و یا از وب سرور دیگری استفاده کرد.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="OAuthWebTest.Login" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body> <form id="form1" runat="server"> <div id="loginform"> <div id="NotLoggedIn" runat="server"> <asp:ImageButton ID="ButtonLoginWithGoogle" runat="server" ImageUrl="Google.JPG" OnCommand="ButtonLoginWithGoogle_Click" CommandArgument="https://www.google.com/accounts/o8/id" /> <p /> <asp:Label runat="server" ID="LabelMessage" /> </div> </div> </form> </body> </html>
using System; using System.Web.UI; using System.Web.UI.WebControls; using DotNetOpenAuth.OpenId.RelyingParty; namespace OAuthWebTest { public partial class Login : Page { protected void Page_Load(object sender, EventArgs e) { OpenIdRelyingParty openIdRelyingParty = new OpenIdRelyingParty(); var response = openIdRelyingParty.GetResponse(); if (response == null) return; switch (response.Status) { case AuthenticationStatus.Authenticated: NotLoggedIn.Visible = false; Session["GoogleIdentifier"] = response.ClaimedIdentifier.ToString(); Response.Redirect("Default.aspx"); break; case AuthenticationStatus.Canceled: LabelMessage.Text = "Cancelled."; break; case AuthenticationStatus.Failed: LabelMessage.Text = "Login Failed."; break; } } protected void ButtonLoginWithGoogle_Click(object src, CommandEventArgs e) { string discoveryUri = e.CommandArgument.ToString(); OpenIdRelyingParty openId = new OpenIdRelyingParty(); var returnToUrl = new UriBuilder(Request.Url) { Query = "" }; var request = openId.CreateRequest(discoveryUri, returnToUrl.Uri, returnToUrl.Uri); request.RedirectToProvider(); } } }
برپایی تنظیمات اولیهی سیستم مسیریابی در AngularJS 2.0
برای کار با سیستم مسیریابی AngularJS 2.0، ابتدا باید اسکریپتهای آن به صفحه اضافه شوند. در ادامه المان پایهای تعریف شده و سپس باید سرویس پروایدر مسیریابی را رجیستر کرد. جزئیات این موارد را در ادامه بررسی میکنیم:
الف) سرویس مسیریابی، جزئی از angular2/core نیست. به همین جهت مدخل اسکریپت متناظر با آن باید به صفحهی اصلی سایت اضافه شود که این مورد، در قسمت اول بررسی پیشنیازهای نصب AngularJS 2.0 صورت گرفتهاست:
<!-- Required for routing --> <script src="~/node_modules/angular2/bundles/router.dev.js"></script>
ب) افزودن المان base به ابتدای صفحه:
<!DOCTYPE html> <html> <head> <base href="/">
از آنجائیکه فایل index.html در ریشهی سایت قرار گرفتهاست، مقدار آغازین href آن به / تنظیم شدهاست.
ج) شبیه به حالت ثبت پروایدر HTTP در قسمت قبل، برای ثبت پروایدر مسیریابی نیز به فایل App\app.component.ts مراجعه میکنیم:
//same as before ... import { ROUTER_PROVIDERS } from 'angular2/router'; //same as before ... @Component({ //same as before … providers: [ ProductService, HTTP_PROVIDERS, ROUTER_PROVIDERS ] }) //same as before ...
علت ختم شدن نام این سرویسها به PROVIDERS این است که این تعاریف، امکان استفادهی از چندین سرویس زیر مجموعهی آنها را فراهم میکنند و صرفا یک سرویس نیستند.
ساخت کامپوننت نمایش جزئیات محصولات
در ادامه میخواهیم جزئیات هر محصول را با کلیک بر روی نام آن در لیست محصولات، در آدرسی دیگر به صورتی مجزا مشاهده کنیم. به همین منظور به پوشهی products برنامه مراجعه کرده و دو فایل جدید product-detail.component.ts و product-detail.component.html را ایجاد کنید؛ با این محتوا:
الف) محتوای فایل product-detail.component.html
<div class='panel panel-primary'> <div class='panel-heading'> {{pageTitle}} </div> </div>
import { Component } from 'angular2/core'; @Component({ templateUrl: 'app/products/product-detail.component.html' }) export class ProductDetailComponent { pageTitle: string = 'Product Detail'; }
اگر دقت کنید، این کامپوننت ویژه دارای خاصیت selector نیست. ذکر selector تنها زمانی اجباری است که بخواهیم این کامپوننت را داخل کامپوننتی دیگر قرار دهیم. در اینجا قصد داریم این کامپوننت را به صورت یک View جدید، توسط سیستم مسیریابی نمایش دهیم و نه به صورت جزئی از یک کامپوننت دیگر.
افزودن تنظیمات مسیریابی به برنامه
مسیریابی در AngularJS 2.0، مبتنی بر کامپوننتها است. به همین جهت، ابتدای کار مسیریابی، مشخص سازی تعدادی از کامپوننتها هستند که قرار است به عنوان مقصد سیستم راهبری (navigation) مورد استفاده قرار گیرند و به ازای هر کدام، یک مسیریابی و Route جدید را تعریف میکنیم. تعریف هر Route جدید شامل انتساب نامی به آن، تعیین مسیر مدنظر و مشخص سازی کامپوننت مرتبط است:
{ path: '/products', name: 'Products', component: ProductListComponent },
این تنظیمات به عنوان یک متادیتای جدید دیگر به کلاس AppComponent، در فایل app.component.ts اضافه میشوند:
//same as before … import { ROUTER_PROVIDERS, RouteConfig } from 'angular2/router'; //same as before … @Component({ //same as before … }) @RouteConfig([ { path: '/welcome', name: 'Welcome', component: WelcomeComponent, useAsDefault: true }, { path: '/products', name: 'Products', component: ProductListComponent } ]) export class AppComponent { pageTitle: string = "DNT AngularJS 2.0 APP"; }
همانطور که ملاحظه میکنید، یک کلاس میتواند بیش از یک decorator داشته باشد.
()RouteConfig@ را به کامپوننتی الصاق میکنیم که قصد میزبانی مسیریابی را دارد (Host component). این مزین کننده، آرایهای از اشیاء را قبول میکند و هر شیء آن دارای خواصی مانند مسیر، نام و کامپوننت است. باید دقت داشت که نام هر مسیریابی تعریف شده باید pascal case باشد. در غیراینصورت مسیریاب ممکن است این نام را با path اشتباه کند.
همچنین امکان تعریف خاصیت دیگری به نام useAsDefault نیز در اینجا میسر است. از آن جهت تعریف مسیریابی پیش فرض سیستم، در اولین بار نمایش آن، استفاده میشود.
در اینجا نام کامپوننت، رشتهای ذکر نمیشود و دقیقا اشاره دارد به نام کلاس متناظر. بنابراین هر نام کلاسی که در اینجا اضافه میشود، باید به همراه import ماژول آن نیز در ابتدای فایل جاری باشد. به همین جهت اگر تنظیمات فوق را اضافه کنید، ذیل کلمهی WelcomeComponent یک خط قرمز مبتنی بر عدم تعریف آن کشیده میشود. برای تعریف آن، پوشهی جدیدی را به ریشهی سایت به نام home اضافه کنید و به آن دو فایل ذیل را اضافه نمائید:
الف) محتوای فایل welcome.component.ts
import { Component } from 'angular2/core'; @Component({ templateUrl: 'app/home/welcome.component.html' }) export class WelcomeComponent { public pageTitle: string = "Welcome"; }
<div class="panel panel-primary"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body"> <h3 class="text-center">Default page</h3> </div> </div>
پس از تعریف این کامپوننت، اکنون باید import ماژول آنرا به ابتدای فایل app.component.ts اضافه کنیم، تا مشکل عدم شناسایی نام کلاس WelcomeComponent برطرف شود:
import { WelcomeComponent } from './home/welcome.component';
فعال سازی مسیریابیهای تعریف شده
روشهای مختلفی برای دسترسی به اجزای یک برنامه وجود دارند؛ برای مثال کلیک بر روی یک لینک، دکمه و یا تصویر و سپس فعال سازی مسیریابی متناظر با آن. همچنین کاربر میتواند آدرس صفحهای را مستقیما در نوار آدرسهای مرورگر وارد کند. به علاوه امکان کلیک بر روی دکمههای back و forward مرورگر نیز همواره وجود دارند. تنظیمات مسیریابیهای انجام شده، دو مورد آخر را به صورت خودکار مدیریت میکنند. در اینجا تنها باید مدیریت اولین حالت ذکر شده را با اتصال مسیریابیها به اعمال کاربران، انجام داد.
به همین جهت منویی را به بالای صفحهی برنامه اضافه میکنیم. برای این منظور، فایل app.component.ts را گشوده و خاصیت template کامپوننت AppComponent را به نحو ذیل تغییر میدهیم:
@Component({ //same as before … template: ` <div> <nav class='navbar navbar-default'> <div class='container-fluid'> <a class='navbar-brand'>{{pageTitle}}</a> <ul class='nav navbar-nav'> <li><a [routerLink]="['Welcome']">Home</a></li> <li><a [routerLink]="['Products']">Product List</a></li> </ul> </div> </nav> <div class='container'> <router-outlet></router-outlet> </div> </div> `, //same as before … })
سپس جهت تعریف لینکهای هر آیتم، از یک دایرکتیو توکار AngularJS 2.0 به نام routerLink استفاده میکنیم. هر routerLink به یکی از آیتمهای تنظیم شدهی در RouteConfig بایند میشود. بنابراین نامهایی که در اینجا قید شدهاند، دقیقا نامهایی هستند که در خاصیت name هر کدام از اشیاء تشکیل دهندهی RouteConfig، تعریف و مقدار دهی گردیدهاند.
اکنون اگر کاربر بر روی یکی از لینکهای Home و یا Product List کلیک کند، مسیریابی متناظر با آن فعال میشود (بر اساس این نام، در لیست عناصر RouteConfig جستجویی صورت گرفته و عنصر معادلی بازگشت داده میشود) و سپس View آن کامپوننت نمایش داده خواهد شد.
تا اینجا دایرکتیو جدید routerLink به قالب کامپوننت اضافه شدهاست؛ اما AngularJS 2.0 نمیداند که باید آنرا از کجا دریافت کند. به همین جهت ابتدا import آنرا (ROUTER_DIRECTIVES) به ابتدای ماژول جاری اضافه خواهیم کرد:
import { ROUTER_PROVIDERS, RouteConfig, ROUTER_DIRECTIVES } from 'angular2/router';
directives: [ROUTER_DIRECTIVES],
تا اینجا اگر دقت کرده باشید، کامپوننت نمایش لیست محصولات را از کامپوننت ریشهی سایت حذف کردهایم و بجای آن منوی بالای سایت را نمایش میدهیم که توسط آن میتوان به صفحهی آغازین و یا صفحهی نمایش لیست محصولات، رسید. به همین جهت خاصیت directives دیگر شامل ذکر کلاس کامپوننت لیست محصولات نیست.
در انتهای قالب کامپوننت ریشهی سایت، یک دایرکتیو جدید به نام router-outlet نیز تعریف شدهاست. وقتی یک کامپوننت فعال میشود، نیاز است View مرتبط با آن نیز نمایش داده شود. دایرکتیو router-outlet محل نمایش این View را مشخص میکند.
اکنون اگر برنامه را اجرا کنیم، به این شکل خواهیم رسید:
اگر دقت کنید، آدرس بالای صفحه، در اولین بار نمایش آن به http://localhost:2222/welcome تنظیم شده و این مقدار دهی بر اساس خاصیت useAsDefault تنظیمات مسیریابی سایت انجام شدهاست (نمایش welcome به عنوان صفحهی اصلی و پیش فرض).
همچنین با کلیک بر روی لینک لیست محصولات، کامپوننت آن فعال شده و نمایش داده میشود. محل قرارگیری این کامپوننتها، دقیقا در محل قرارگیری دایرکتیو router-outlet است.
ارسال پارامترها به سیستم مسیریابی
در ابتدا بحث، مقدمات کامپوننت نمایش جزئیات یک محصول انتخابی را تهیه کردیم. برای فعال سازی این کامپوننت و مسیریابی آن، نیاز است بتوان پارامتری را به سیستم مسیریابی ارسال کرد. برای مثال با انتخاب آدرس product/5، جزئیات محصول با ID مساوی 5 نمایش داده شود.
برای این منظور:
الف) اولین قدم، تعریف مسیریابی آن است. به همین جهت به فایل app.component.ts مراجعه و دو تغییر ذیل را به آن اعمال کنید:
//same as before … import { ProductDetailComponent } from './products/product-detail.component'; @Component({ //same as before … }) @RouteConfig([ //same as before … { path: '/product/:id', name: 'ProductDetail', component: ProductDetailComponent } ]) //same as before …
تفاوت این مسیریابی با نمونههای قبلی در تعریف id:/ است. پس از ذکر :/، نام یک متغیر عنوان میشود و اگر نیاز به چندین متغیر بود، همین الگو را تکرار خواهیم کرد.
ب) سپس نحوهی فعال سازی این مسیریابی را توسط تعریف لینکی جدید، معرفی میکنیم. بنابراین فایل قالب product-list.component.html را گشوده و سپس بجای نمایش عنوان محصول:
<td>{{ product.productName }}</td>
<td> <a [routerLink]="['ProductDetail', {id: product.productId}]"> {{product.productName}} </a> </td>
اکنون که از دایرکتیو جدید routerLink در این قالب استفاده شدهاست، نیاز است تعریف دایرکتیو آنرا به متادیتای کلاس کامپوننت لیست محصولات نیز اضافه کنیم تا AngularJS 2.0 بداند آنرا از کجا باید تامین کند:
import { Component, OnInit } from 'angular2/core'; import { ROUTER_DIRECTIVES } from 'angular2/router'; //same as before … @Component({ //same as before … directives: [StarComponent, ROUTER_DIRECTIVES] })
در ادامه اگر برنامه را اجرا کنید، عنوانهای محصولات، به آدرس نمایش جزئیات آنها لینک شدهاند:
ج) در آخر زمانیکه View نمایش جزئیات محصول فعال میشود، نیاز است این id را از url جاری دریافت کند. به همین جهت فایل product-detail.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { Component } from 'angular2/core'; import { RouteParams } from 'angular2/router'; @Component({ templateUrl: 'app/products/product-detail.component.html' }) export class ProductDetailComponent { pageTitle: string = 'Product Detail'; constructor(private _routeParams: RouteParams) { let id = +this._routeParams.get('id'); this.pageTitle += `: ${id}`; } }
در این حالت، id دریافتی، به متغیر pageTitle اضافه شده و در قالب مربوطه به صورت خودکار نمایش داده میشود.
تا اینجا اگر برنامه را اجرا کنید، صفحهی نمایش جزئیات یک محصول، با کلیک بر روی عناوین آنها به صورت زیر نمایش داده میشود:
افزودن دکمهی back با کدنویسی
اکنون برای بازگشت مجدد به لیست محصولات، میتوان از دکمهی back مرورگر استفاده کرد، اما امکان طراحی این دکمه در قالبها نیز پیش بینی شدهاست.
برای این منظور قالب product-detail.component.html را به نحو ذیل بازنویسی میکنیم:
<div class='panel panel-primary'> <div class='panel-heading'> {{pageTitle}} </div> <div class='panel-footer'> <a class='btn btn-default' (click)='onBack()' style='width:80px'> <i class='glyphicon glyphicon-chevron-left'></i> Back </a> </div> </div>
سپس کدهای product-detail.component.ts را به صورت ذیل تکمیل خواهیم کرد:
import { Component } from 'angular2/core'; import { RouteParams, Router } from 'angular2/router'; @Component({ templateUrl: 'app/products/product-detail.component.html' }) export class ProductDetailComponent { pageTitle: string = 'Product Detail'; constructor(private _routeParams: RouteParams, private _router: Router) { let id = +this._routeParams.get('id'); this.pageTitle += `: ${id}`; } onBack(): void { this._router.navigate(['Products']); } }
رفع تداخل مسیریابیهای ASP.NET MVC با مسیریابیهای AngularJS 2.0
در طی بحث جاری عنوان شد که اگر کاربر مسیر http://localhost:2222/product/2 را جایی ثبت کرده یا bookmark کند، پس از فراخوانی مستقیم آن در نوار آدرسهای مرورگر، بلافاصله به این آدرس هدایت خواهد شد. این مورد صحیح است اگر از index.html بجای بکارگیری ASP.NET MVC، جهت هاست برنامه استفاده شود. اگر چنین آدرسی را در یک برنامهی ASP.NET MVC فراخوانی کنیم، ابتدا به دنبال کنترلری به نام product میگردد (ابتدا وارد موتور ASP.NET MVC میشود) و چون این کنترلر در سمت سرور تعریف نشدهاست، پیام 404 و یا یافت نشد را مشاهده خواهید کرد و فرصت به اجرای برنامهی AngularJS نخواهد رسید.
برای حل این مشکل نیاز است یک route جدید را به نام catch all، در انتهای مسیریابیهای فعلی اضافه کنید؛ تا سایر درخواستهای رسیده را به صفحهی نمایش برنامهی AngularJS هدایت کند:
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints: new { controller = "Home" } // for catch all to work, Home|About|SomeName ); // Route override to work with Angularjs and HTML5 routing routes.MapRoute( name: "NotFound", url: "{*catchall}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part9.zip
خلاصهی بحث
حین ایجاد کامپوننتها باید به نحوهی نمایش آنها نیز فکر کرد. اگر کامپوننتی قرار است داخل یک کامپوننت دیگر نمایش یابد، باید دارای selector باشد. یک چنین کامپوننتی نیاز به تعریف مسیریابی ندارد. برای کامپوننتهایی که به عنوان یک View مستقل طراحی میشوند و قرار است در یک صفحهی مجزا نمایش داده شوند، نیازی به تعریف selector نیست؛ اما باید برای آنها مسیریابیهای ویژهای را تعریف کرد. همچنین نیاز است مدیریت اعمال کاربران را جهت فعال سازی آنها نیز مدنظر داشت. برای استفاده از امکانات مسیریابی توکار AngularJS 2.0 نیاز است اسکریپت آنرا به صفحهی اصلی اضافه کرد. سپس باید المان base را جهت نحوهی تشکیل آدرسهای مسیریابی، به صفحه اضافه کرد. در ادامه کار ثبت ROUTER_PROVIDERS در بالاترین سطح سلسه مراتب کامپوننتهای سایت انجام میشود. با استفاده از RouteConfig کار تنظیمات ابتدایی مسیریابی صورت خواهد گرفت. این decorator به کامپوننتی که قرار است کار میزبانی مسیریابی را انجام دهد، متصل میشود. پس از تعریف مسیریابیها با ذکر یک نام منحصربفرد، مسیری که باید توسط کاربر وارد شود و نام کامپوننت مدنظر، با استفاده از دایرکتیو routerLink کار تعریف این آدرسها، در رابط کاربری برنامه انجام میشود. این دایرکتیو جدید، جزئی از مجموعهی ROUTER_DIRECTIVES است که باید به لیست دایرکتیوهای کامپوننت ریشههای سایت و هر کامپوننتی که از routeLink استفاده میکند، اضافه شود. برای بایند این دایرکتیو به مسیریابیهای تعریف شده، سمت راست این اتصال باید به آرایهای از مقادیر مقدار دهی شود که اولین عنصر آن، نام یکی از عناصر مسیریابی تعریف شدهی در RouteConfig است. از دومین عنصر آن برای مقدار دهی پارامترهای ارسالی به سیستم مسیریابی استفاده میشود. کار دایرکتیو router-outlet، مشخص سازی محل نمایش یک View است که عموما در قالب میزبان مسیریابی قرار میگیرد. برای تعیین پارامترهای مسیریابی، از الگوی paramName:/ استفاده میشود. برای دسترسی به این مقادیر در یک کامپوننت، میتوان از سرویس RouteParams استفاده کرد. برای فعال سازی یک مسیریابی با کدنویسی، از سرویس Router و متد navigate آن کمک میگیریم.
1) دریافت کتابخانههای لازم
نیاز به کتابخانههای Lucene.NET و همچنین Lucene.Net Contrib است که هر دو مورد را به سادگی توسط NuGet میتوانید دریافت و نصب کنید.
Highlighter استفاده شده، در کتابخانه Lucene.Net Contrib قرار دارد. به همین جهت این مورد را نیز باید جداگانه دریافت کرد.
2) تهیه منبع داده
در اینجا جهت سادگی کار فرض کنید که لیستی از مطالب را به فرمت زیر دراختیار داریم:
public class Post { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } }
3) تبدیل اطلاعات به فرمت Lucene.NET
همانطور که عنوان شد نیاز است هر رکورد از اطلاعات خود را به شیء Document نگاشت کنیم. نمونهای از اینکار را در متد ذیل مشاهده مینمائید:
static Document MapPostToDocument(Post post) { var postDocument = new Document(); postDocument.Add(new Field("Id", post.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); postDocument.Add(new Field("Title", post.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); postDocument.Add(new Field("Body", post.Body, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); return postDocument; }
کار با ایجاد یک وهله از شیء Document شروع شده و سپس اطلاعات به صوت فیلدهایی به این سند اضافه میشوند.
توضیحات آرگومانهای مختلف سازنده کلاس Field:
- در ابتدا نام فیلد مورد نظر ذکر میگردد.
- سپس مقدار متناظر با آن فیلد، به صورت رشته باید معرفی شود.
- آرگومان سوم آن مشخص میکند که اصل اطلاعات نیز علاوه بر ایندکس شدن باید در فایلهای Lucene ذخیره شوند یا خیر. توسط Field.Store.YES مشخص میکنیم که بله؛ علاقمندیم تا اصل اطلاعات نیز از طریق Lucene قابل بازیابی باشند. این مورد جهت نمایش سریع نتایج جستجوها میتواند مفید باشد. اگر قرار نیست اطلاعاتی را از این فیلد خاص به کاربر نمایش دهید میتوانید از گزینه Field.Store.NO استفاده کنید. همچنین امکان فشرده سازی اطلاعات ذخیره شده با انتخاب گزینه Field.Store.COMPRESS نیز میسر است.
- توسط آرگومان چهارم آن تعیین خواهیم کرد که اطلاعات فیلد مورد نظر ایندکس شوند یا خیر. مقدار Field.Index.NOT_ANALYZED سبب عدم ایندکس شدن فیلد Id میشوند (چون قرار نیست روی id در قسمت جستجوی عمومی سایت، جستجویی صورت گیرد). به کمک مقدار Field.Index.ANALYZED، مقدار معرفی شده، ایندکس خواهد شد.
- پارامتر پنجم آنرا جهت سرعت عمل در نمایان سازی/برجسته کردن و highlighting عبارات جستجو شده در متنهای یافت شده معرفی کردهایم. الگوریتمهای متناظر با این روش در فایلهای Lucene.Net Contrib قرار دارند.
یک نکته
اگر اطلاعاتی را که قرار است ایندکس کنید از نوع HTML میباشند، بهتر است تمام تگهای آنرا پیش از افزودن به لوسین حذف کنید. به این ترتیب نتایج جستجوی دقیقتری را میتوان شاهد بود. برای این منظور میتوان از متد ذیل کمک گرفت:
public static string RemoveHtmlTags(string text) { return string.IsNullOrEmpty(text) ? string.Empty : Regex.Replace(text, @"<(.|\n)*?>", string.Empty); }
4) تهیه Full text index به کمک Lucene.NET
تا اینجا توانستیم اطلاعات خود را به فرمت اسناد لوسین تبدیل کنیم. اکنون ثبت و تبدیل آنها به فایلهای Full text search لوسین به سادگی زیر است:
static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_29; public static void CreateIdx(IEnumerable<Post> dataList) { var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex")); var analyzer = new StandardAnalyzer(_version); using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED)) { foreach (var post in dataList) { writer.AddDocument(MapPostToDocument(post)); } writer.Optimize(); writer.Commit(); writer.Close(); directory.Close(); } }
ذکر version در اینجا ضروری است؛ از این جهت که اگر ایندکسی با فرمت مثلا LUCENE_29 تهیه شود ممکن است با نگارش بعدی این کتابخانه سازگار نباشد و در صورت ارتقاء، نتایج جستجوی انجام شده، کاملا بیربط نمایش داده شوند. با ذکر صریح نگارش، دیگر این اتفاق رخ نخواهد داد.
نکته
StandardAnalyzer توکار لوسین، امکان دریافت لیستی از واژههایی که نباید ایندکس شوند را نیز دارا است. اطلاعات بیشتر در اینجا.
5) به روز رسانی ایندکسها
به کمک سه متد ذیل میتوان اطلاعات ایندکسهای موجود را به روز یا حذف کرد:
public static void UpdateIndex(Post post) { var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex")); var analyzer = new StandardAnalyzer(_version); using (var indexWriter = new IndexWriter(directory, analyzer, create: false, mfl: IndexWriter.MaxFieldLength.UNLIMITED)) { var newDoc = MapPostToDocument(post); indexWriter.UpdateDocument(new Term("Id", post.Id.ToString()), newDoc); indexWriter.Commit(); indexWriter.Close(); directory.Close(); } } public static void DeleteIndex(Post post) { var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex")); var analyzer = new StandardAnalyzer(_version); using (var indexWriter = new IndexWriter(directory, analyzer, create: false, mfl: IndexWriter.MaxFieldLength.UNLIMITED)) { indexWriter.DeleteDocuments(new Term("Id", post.Id.ToString())); indexWriter.Commit(); indexWriter.Close(); directory.Close(); } } public static void AddIndex(Post post) { var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex")); var analyzer = new StandardAnalyzer(_version, getStopWords()); using (var indexWriter = new IndexWriter(directory, analyzer, create: false, mfl: IndexWriter.MaxFieldLength.UNLIMITED)) { var searchQuery = new TermQuery(new Term("Id", post.Id.ToString())); indexWriter.DeleteDocuments(searchQuery); var newDoc = MapPostToDocument(post); indexWriter.AddDocument(newDoc); indexWriter.Commit(); indexWriter.Close(); directory.Close(); } }
محل فراخوانی این متدها هم میتواند در کنار متدهای به روز رسانی اطلاعات اصلی در بانک اطلاعاتی برنامه باشند. اگر رکوردی اضافه یا حذف شده، ایندکس متناظر نیز باید به روز شود.
6) جستجو در اطلاعات ایندکس شده و نمایش آنها به همراه نمایان/برجسته سازی عبارات جستجو شده
قسمت نهایی کار با لوسین و اطلاعات ایندکسهای تهیه شده، کوئری گرفتن از آنها است. متدهای کامل مورد نیاز را در ذیل مشاهده میکنید:
public static void Query(string term) { var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex")); using (var searcher = new IndexSearcher(directory, readOnly: true)) { var analyzer = new StandardAnalyzer(_version); var parser = new MultiFieldQueryParser(_version, new[] { "Body", "Title" }, analyzer); var query = parseQuery(term, parser); var hits = searcher.Search(query, 10).ScoreDocs; if (hits.Length == 0) { term = searchByPartialWords(term); query = parseQuery(term, parser); hits = searcher.Search(query, 10).ScoreDocs; } FastVectorHighlighter fvHighlighter = new FastVectorHighlighter(true, true); foreach (var scoreDoc in hits) { var doc = searcher.Doc(scoreDoc.doc); string bestfragment = fvHighlighter.GetBestFragment( fvHighlighter.GetFieldQuery(query), searcher.GetIndexReader(), docId: scoreDoc.doc, fieldName: "Body", fragCharSize: 400); var id = doc.Get("Id"); var title = doc.Get("Title"); var score = scoreDoc.score; Console.WriteLine(bestfragment); } searcher.Close(); directory.Close(); } } private static Query parseQuery(string searchQuery, QueryParser parser) { Query query; try { query = parser.Parse(searchQuery.Trim()); } catch (ParseException) { query = parser.Parse(QueryParser.Escape(searchQuery.Trim())); } return query; } private static string searchByPartialWords(string bodyTerm) { bodyTerm = bodyTerm.Replace("*", "").Replace("?", ""); var terms = bodyTerm.Trim().Replace("-", " ").Split(' ') .Where(x => !string.IsNullOrEmpty(x)) .Select(x => x.Trim() + "*"); bodyTerm = string.Join(" ", terms); return bodyTerm; }
اکثر سایتها را که بررسی کنید، جستجوی بر روی یک فیلد را توضیح دادهاند. در اینجا نحوه جستجو بر روی چند فیلد را به کمک MultiFieldQueryParser ملاحظه میکنید.
نکتهی مهمی را هم که در اینجا باید به آن دقت داشت، حساس بودن لوسین به کوچکی و بزرگی نام فیلدهای معرفی شده است و در صورت عدم رعایت این مساله، جستجوی شما نتیجهای را دربر نخواهد داشت.
در ادامه برای parse اطلاعات، از متد کمکی parseQuery استفاده شده است. ممکن است به ParseException بخاطر یک سری حروف خاص بکارگرفته شده در عبارات مورد جستجو برسیم. در اینجا میتوان توسط متد QueryParser.Escape، اطلاعات دریافتی را اصلاح کرد.
سپس نحوه استفاده از کوئری تهیه شده و متد Search را ملاحظه میکنید. در اینجا بهتر است تعداد رکوردهای بازگشت داده شده را تعیین کرد (به کمک آرگومان دوم متد جستجو) تا بیجهت سرعت عملیات را پایین نیاورده و همچنین مصرف حافظه سیستم را نیز بالا نبریم.
ممکن است تعداد hits یا نتایج حاصل صفر باشد؛ بنابراین بد نیست خودمان دست به کار شده و به کمک متد searchByPartialWords، ورودی کاربر را بر اساس زبان جستجوی ویژه لوسین اندکی بهینه کنیم تا بتوان به نتایج بهتری دست یافت.
در آخر نحوه کار با ScoreDocs یافت شده را ملاحظه میکنید. اگر محتوای فیلد را در حین ایندکس سازی ذخیره کرده باشیم، به کمک متد doc.Get میتوان به اطلاعات کامل آن نیز دست یافت.
همچنین نکته دیگری را که در اینجا میتوان ملاحظه کرد استفاده از FastVectorHighlighter میباشد. به کمک این Highlighter ویژه میتوان نتایج جستجو را شبیه به نتایج نمایش داده شده توسط موتور جستجوی گوگل درآورد. برای مثال اگر شخصی ef code first را جستجو کرد، توسط متد GetBestFragment، بهترین جزئی که شامل بیشترین تعداد حروف جستجو شده است، یافت گردیده و همچنین به کمک تگهای B، ضخیم نمایش داده خواهند شد.