اشتراک‌ها
QuickChart، ایجاد نمودارهای تصویری.

Static embeddable image charts for email, SMS, reports, and more 

<img src="https://quickchart.io/chart?width=500&height=300&c={type:'bar',data:{labels:['January','February','March','April', 'May'], datasets:[{label:'Dogs',data:[50,60,70,180,190]},{label:'Cats',data:[100,200,300,400,500]}]}}">


QuickChart، ایجاد نمودارهای تصویری.
مطالب
تقسیم جدول در Entity Framework Code First
سناریو هایی هستند که در آن ها، تعداد ستون‌های یک جدول، بیش از اندازه زیاد می‌شوند و یا آن جدول حاوی فیلدهایی هست که منابع زیادی مصرف می‌کنند، به مانند فیلدهای متنی طولانی یا عکس. معمولا متوجه می‌شویم که در اکثر مواقع، به هنگام واکشی اطلاعات آن جدول، احتیاجی به داده‌های آن فیلد‌ها نداریم و با واکشی بی مورد آن ها، سربار اضافه ای به سیستم تحمیل می‌کنیم، چرا که این داده‌ها ، منابع حافظه ای ما را به هدر می‌دهند.
برای مثال، جدول 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);
            }
خروجی زیر را در SQL Server Profiler مشاهده خواهید کرد:

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; }
    }
}
همان طور که می‌بینید، خواص حجیم به جدول دیگری به نام PostMetaData منتقل شده و با تعریف خواص راهبری ارجاعی در هر دو کلاس،رابطه‌ی یک به یک بین آن‌ها برقرار شده است.جز الزامات تعریف روابط یک به یک این است که، با استفاده از Fluent API یا Data Annotations ، طرف‌های Depenedent و Principal، صریحا به EF معرفی شوند.

namespace SplittingTableSample.DomainClasses
{
    public class PostMetaDataConfig : EntityTypeConfiguration<PostMetaData>
    {
        public PostMetaDataConfig()
        {
            HasKey(x => x.PostId);
            HasRequired(x => x.Post).WithRequiredDependent(x => x.PostMetaData);
        }
    }
}
اولین نکته ای که باید به آن توجه شود، این است که در کلاس PostMetaData، قوانین پیش فرض EF برای تعیین کلید اصلی نقض شده است و به همین دلیل، صراحتا با استفاده از متد HasKey ، کلید اصلی به EF معرفی شده است. نکته‌ی مهم دیگری که به آن باید توجه شود این است که هر دو سر رابطه به صورت Required تعریف شده است. دلیل این موضوع هم با توجه به مطلبی که قرار است گفته شود،کمی جلوتر خواهید فهمید. حال اگر تعاریف DbSet‌ها را نیز اصلاح کنیم و دستور زیر را اجرا کنیم:

var post = context.Posts.Find(1);
خروجی sql زیر را مشاهده خواهید کرد:

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
خیلی خوب! دیگر خبری از  فیلدهای اضافی Body و Image نیست. دلیل اینکه در اینجا join  بین دو جدول مشاهده نمی‌شود، قابلیت lazy loading است، که با virtual تعریف کردن خواص راهبری حاصل شده است. پس lazy loading در اینجا واقعا مفید است.
اما راه حل ذکر شده نیز کاملا بدون ایراد نیست. مشکل اساسی آن تعدد تعداد جداول آن است. آیا جدول 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);
        }
    }
}
نکته مهم این است که در هر دو کلاس(حتی کلاس Post) باید با استفاده از متد ToTable، کلاس‌ها را به یک نام در دیتابیس نگاشت کنیم. در نتیجه با استفاده از متد ToTable در هر دو موجودیت، آنها در دیتابیس به جدولی به نام  Posts نگاشت خواهند شد. تصویر زیر پس از اجرای برنامه، بیان گر این موضوع خواهد بود.

اگر دستورات زیر را اجرا کنید:


var post = context.Posts.Find(1);
Console.WriteLine(post.PostMetaData.Body);
خروجی زیر را در  SQL Server Profiler مشاهده خواهید کرد:
برای متد 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
و برای post.PostMetaData.Body دستور sql زیر را مشاهده می‌کنید:

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
دلیل این که در اینجا ،دو دستور sql به دیتابیس ارسال شده است، فعال بودن ویژگی lazy loading ،به دلیل virtual  تعریف کردن خواص راهبری موجودیت‌ها است.
حال اگر بخواهیم با یک رفت و آمد به دیتابیس کلیه اطلاعات را واکشی کنیم، می‌توانیم از Eager Loading استفاده کنیم:
 
var post = context.Posts.Include(x => x.PostMetaData).SingleOrDefault(x => x.Id == 1);
که خروجی sql آن نیز به شکل زیر است:

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]
در نتیجه با کمک این تکنیک توانستیم، با چند موجودیت، در قالب یک جدول رفتار کنیم و از مزیت‌های آن همچون lazy loading، نیز بهره مند شویم.

دریافت کد‌های این بخش: SplittingTable-Sample.rar 
مطالب
تغییرات بوجود آمده در Razor -MVC4
همانطوری که میدونید نسخه    MVC 4 RC   در دسترس قرار گرفته و خالی از لطف نیست که یک بررسی درباره امکانات جدیدش انجام بشه.ابتدا سعی میکنم یک لیست کلی از امکانان این تکنولوژی داشته باشیم و بعد نگاهی هم به Razor  و تغییراتش خواهیم داشت.
 
ASP.NET Web API
Refreshed and modernized default project templates
New mobile project template
Many new features to support mobile apps
Recipes to customize code generation
Enhanced support for asynchronous methods 
لیست فوق شامل برترین ویژگی‌های این نسخه است که در پستهای آینده هر کدام از این‌ها بررسی خواهند شد.

تغییرات Razor
Razor از نسخه MVC4 Beta شاهد تغییرات و بهبود هایی بود که این تغییرات بنیادی و رادیکالی نبودند و فقط درجهت بهبود حس کاربری آن صورت گرفت. این حس از آن جهت است که شما نیاز به نوشتن کد کمتری دارید.

استفاده نکردن از  Url.Content@ 
در نسخه قبلی از امکان ذکر شده برای مشخص کرده مسیر‌های CSS و فایلهای .JS هم استفاده میشد حالا بجای استفاده از آن میشود:
در نسخه‌های قبلی:
<script src="@Url.Content("~/Scripts/Site.js")"></script>
 در نسخه 4:
<script src="~/Scripts/Site.js"></script> 
زمانی که Razor  حرف را ~/   تشخیص می‌دهد خروجی یکسانی با حالت قبلی برای ما درست می‌کند.

شرط ها(Conditions)
در نسخه قبلی برای استفاده از attribute ها که ممکن بود Null باشند مجبور به چک کردن آنها بودیم:
<div @{if (myClass != null) { <text>class="@myClass"</text> } }>Content</div>
اما حالا با خیال راحت می‌توان نوشت:
<div class="@myClass">Content</div>
که اگر attribute ما null باشد به صورت اتوماتیک تشخیص داده میشود و کد زیر رندر میشود
<div>Content</div>
در ضمناً کد بالا فقط مربوط به چک کردن Nullable نیست و از آن می‌توان در Boolean‌ها هم استفاده کرد.
<input checked="@ViewBag.Checked" type="checkbox"/>
که اگر مقدار True نباشد:
<input type="checkbox"/>
بطور خلاصه میشه گفت MVC4 تغییراتش نسبت به نسخه قبلی تو خیلی از زمینه‌ها مربوط میشه بهبود ابزارهای موجود در کل کار با این ایزار بسیار برای من لذت بخشه.
ادامه دارد...
مطالب
Vue.js - نصب و راه‌اندازی اولیه - قسمت اول
Vue.js چیست؟
ویو‌جی‌اس یا وو جی اس (Vue.js) یکی از فریم‌ورک‌های جاوااسکریپتی است که به ما در ساختن اپلیکیشن‌های تحت وب و تحت معماری MVVM یا همان (Model–view–viewmodel) کمک می‌کند.
در حالیکه مدت کمی از انتشار Vue.js می‌گذرد، اما این فریم‌ورک به شدت محبوب شده و طرفداران زیادی پیدا کرده است.

پیش‌نیازها :
  • آشنایی کامل با جاوااسکریپت
  • آشنایی با Object-oriented در جاوااسکریپت 

اولین قدم برای استفاده از Vue.js، نصب آن می‌باشد که مرحله به مرحله توضیح خواهم داد. جهت نصب، سایت Vue.js به ما چندین راه پیشنهاد می‌دهد که ما آنها را در ذیل بررسی می‌کنیم:
1- CDN
در قسمت CDN به آدرس زیر مراجعه کرده و لینک آن را در صفحه‌ای که نیاز است از آن استفاده کنیم، قرار می‌دهیم. هرچندکه به صورت لوکال هم می‌توانید کدها را در یک فایل ذخیره کرده و از آن استفاده کنید.
ابتدا باید آدرس زیر، درون تگ < script> قرار بگیرد
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>

2- NPM
طبق همین دستور می‌توانید Vue.js را نصب کنید 
$ npm install vue

3- CLI 
 با استفاده از کامند لاین هم به راحتی قابل نصب می‌باشد؛ با استفاده از دستورات زیر
$ npm install -g vue-cli
$ vue init webpack my-project
$ cd my-project
$ npm install
$ npm run dev

توجه: بهترین روش نصب، استفاده از CDN می‌باشد که توصیه می‌شود جهت سهولت و جلوگیری از دردسرهای نصب فریم‌ورک، از مورد اول استفاده کنید.
حال پس از نصب، چنین کدی آماده خواهد شد.
<html> 
    <body> 

      <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.0/vue.js"></script> 
    </body> 
</html>
آخرین نسخه از ویو جی اس، 2.2.0 است.
مطالب
Persist ، Load و Bookmark در Workflow

در خیلی از مواقع 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;
            }
        }
    }
}
این کلاس را generic نوشتم تا به جای User بشود هر پارامتر دیگه‌ای را به آن ارسال کرد. در واقع وقتی workflow به این Activity می‌رسد، Idle می‌شود. این activity  یک bookmark هم ایجاد می‌کند. ما وقتی workflow را با این bookmark فراخوانی کنیم؛ workflow از همینجا ادامه می‌یابد. فراخوانیbookmark می‌تواند همراه با وارد کردن یک  object باشد. متد Continue آن object را به آرگومان خروجی می‌دهد تا مسیر workflow را طی کند.
ما 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 صورت می‌گیرد. 

مطالب
ایجاد فرم جستجوی پویا با استفاده از Expression ها
در مواردی نیاز است کاربر را جهت انتخاب فیلدهای مورد جستجو آزاد نگه داریم. برای نمونه جستجویی را در نظر بگیرید که کاربر قصد دارد: "دانش آموزانی که نام آنها برابر علی است و شماره دانش آموزی آنها از 100 کمتر است" را پیدا کند در شرایطی که فیلدهای نام و شماره دانش آموزی و عمل گر کوچک‌تر را خود کاربر به دلخواه برگزیرده.
روش‌های زیادی برای پیاده سازی این نوع جستجوها وجود دارد. در این مقاله سعی شده گام‌های ایجاد یک ساختار پایه برای این نوع فرم‌ها و یک ایجاد فرم نمونه بر پایه ساختار ایجاد شده را با استفاده از یکی از همین روش‌ها شرح دهیم.
اساس این روش تولید عبارت 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; }
    }
}
3- در پروژه سرویس یک کلاس بصورت زیر ایجاد می‌کنیم:
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"),
                };
        }
    }
}
4- تا اینجا تمامی داده‌ها صرفا برای نمونه بود. در این مرحله ساخت اساس جستجو گر پویا را شرح می‌دهیم.
جهت ساخت عبارت، نیاز به سه نوع جزء داریم:
-اتصال دهنده عبارات ( "و" ، "یا")
-عملوند (در اینجا فیلدی که قصد مقایسه با عبارت مورد جستجوی کاربر را داریم)
-عملگر ("<" ، ">" ، "=" ، ....)

برای ذخیره المان‌های انتخاب شده توسط کاربر، سه کلاس زیر را ایجاد می‌کنیم (همان سه جزء بالا):
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;
        }
    }
}
5- ایجاد کلاس فیلتر جهت معرفی فیلدها و معرفی منبع داده و ویو مدلی ارث برنده از کلاس‌های پایه ساختار، جهت ایجاد فرم نمونه:

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();
        }
    }
}
6- ایجاد ویو نمونه:

در نهایت زمل فایل موجود در پروژه ویو:

<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
 
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 2 - بررسی ساختار جدید Solution
اگر یک پروژه‌ی خالی ASP.NET Core Web Application را شروع کنید (با طی مراحل زیر جهت ایجاد یک پروژه‌ی جدید):
 .NET Core -> ASP.NET Core Web Application (.NET Core) -> Select `Empty` Template
تغییرات ساختاری ASP.NET Core 1.0، با نگارش‌های قبلی ASP.NET، بسیار قابل ملاحظه هستند:


در اینجا نقش 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" ],
اما در مورد هر مسیری که ذکر می‌شود، NET Core. باید بتواند یک سطح پایین‌تر از پوشه‌ی ذکر شده، فایل مهم project.json را پیدا کند؛ در غیراینصورت از آن صرفنظر خواهد شد. برای مثال برای مسیر نسبی src، مسیر src\MyProjectName\project.json را جستجو می‌کند و برای مسیر مطلق ذکر شده، این مسیر را c:\\sources\\Configuration\\src\\SomeName\\project.json


کامپایل خودکار پروژه در 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!");
});
اکنون در همین حال که برنامه در حال اجرا است و هنوز IIS Express خاتمه نیافته است، از طریق windows explorer، به پوشه‌ی src برنامه وارد شده و فایل Startup.cs را با notepad باز کنید. هدف این است که این فایل را در خارج از ویژوال استودیو ویرایش کنیم. اینبار سطر await دار را در notepad به نحو ذیل ویرایش کنید:
 await context.Response.WriteAsync("Hello DNT!");
پس از آن اگر مرورگر را refresh کنید، بلافاصله خروجی جدید فوق را مشاهده خواهید کرد که بیانگر کامپایل خودکار پروژه در صورت تغییر فایل‌های آن است.
این مساله قابلیت استفاده‌ی از 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"
    ]
  }
},
در اینجا، netcoreapp1.0 به معنای برنامه‌‌ای است که برفراز NET Core. اجرا می‌شود. نام پیشین آن dnxcore50 بود (اگر مقالات قدیمی‌تر پیش از RTM را مطالعه کنید).
در اینجا اگر علاقمند بودید که از دات نت کامل مخصوص ویندوز نیز استفاده کنید، می‌توانید آن‌را در لیست فریم ورک‌ها اضافه نمائید (در این مثال، دات نت کامل 4.5.2 نیز ذکر شده‌است):
 "frameworks": {
    "netcoreapp1.0": {
    },
    "net452": {
    }
  }
لیست کامل این اسامی را در مستندات NET Starndard می‌توانید مطالعه کنید و خلاصه‌ی آن به این صورت است:
- “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": {
      }
  }
},
لیست وابستگی‌های خاص این فریم ورک در خاصیت 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"
  }
}
این نام‌ها استاندارد هستند. برای مثال اگر قصد استفاده‌ی از npm مربوط به node.js را داشته باشید، نام این فایل packag.json است (با ساختار خاص خودش) و هر دوی این‌ها را نیز می‌توانید با هم اضافه کنید و از این لحاظ محدودیتی وجود ندارد.
پس از اضافه شدن فایل bower.json، بلافاصله کار restore بسته‌ها از اینترنت شروع می‌شود:


و یا با کلیک راست بر روی گره dependencies، گزینه‌ی restore packages نیز وجود دارد.
فایل‌های نهایی دریافت شده را در پوشه‌ی bower_components خارج از wwwroot می‌توانید مشاهده کنید.

در مورد نحوه‌ی توزیع و دسترسی به فایل‌های استاتیک یک برنامه‌ی ASP.NET Core 1.0، نکات خاصی وجود دارند که در قسمت‌های بعد، بررسی خواهند شد.

یک نکته: اگر خواستید نام پوشه‌ی wwwroot را تغییر دهید، فایل جدیدی را به نام hosting.json با این محتوا به پروژه اضافه کنید:
{
    "webroot":"AppWebRoot"
}
در اینجا AppWebRoot نام دلخواه پوشه‌ی جدیدی است که نیاز است به ریشه‌ی پروژه اضافه نمائید تا بجای wwwroot پیش فرض استفاده شود.


نقطه‌ی آغازین برنامه کجاست؟

اگر به فایل project.json دقت کنید، چنین تنظیمی در آن موجود است:
"buildOptions": {
  "emitEntryPoint": true,
  "preserveCompilationContext": true
},
true بودن مقدار تولید entry point به استفاده‌ی از فایلی به نام Program.cs بر می‌گردد؛ با این محتوا:
public static void Main(string[] args)
{
    var host = new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseIISIntegration()
        .UseStartup<Startup>()
        .Build();
 
    host.Run();
}
به این ترتیب، یک برنامه‌ی ASP.NET Core، دقیقا همانند سایر برنامه‌های NET Core. رفتار می‌کند و در اساس یک برنامه‌ی کنسول است.
 var outputKind = compilerOptions.EmitEntryPoint.GetValueOrDefault() ?
OutputKind.ConsoleApplication : OutputKind.DynamicallyLinkedLibrary;
این چند سطر، قسمتی از سورس کد ASP.NET Core 1.0 هستند. به این معنا که اگر مقدار خاصیت emitEntryPoint مساوی true بود، با این برنامه به شکل یک برنامه‌ی کنسول رفتار کن و در غیر اینصورت یک Dynamically Linked Library خواهد بود.
در فایل 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!");
        });
    }
}
در اینجا امکان دسترسی به تنظیمات برنامه، معرفی سرویس‌ها، middleware‌ها و غیره وجود دارند که هرکدام، در قسمت‌های آتی به تفصیل بررسی خواهند شد.


نقش فایل 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"
      }
    }
  }
}
همانطور که مشاهده می‌کنید، در اینجا تنظیمات IIS Express قابل تغییر هستند. برای مثال port پیش فرض برنامه 7742 تنظیم شده‌است.
پروفایل‌هایی که در اینجا ذکر شده‌اند، در تنظیمات پروژه نیز قابل مشاهده هستند: (کلیک راست بر روی پروژه و مشاهده‌ی properties آن و یا دوبار کلیک بر روی گره properties)


به علاوه امکان انتخاب این پروفایل‌ها در زمان آغاز برنامه نیز وجود دارند:


نکته‌ی مهم تمام این موارد به قسمت environment variable قابل مشاهده‌ی در تصاویر فوق بر می‌گردد. این متغیر محیطی می‌تواند سه مقدار Development ، Staging و Production را داشته باشد و بر اساس این متغیر و مقدار آن، می‌توان پروفایل جدیدی را تشکیل داد. زمانیکه برنامه بر اساس پروفایل خاصی بارگذاری می‌شود، اینکه چه متغیر محیطی انتخاب شده‌است، در کلاس Startup قابل استخراج و بررسی بوده و بر این اساس می‌توان اقدامات خاصی را انجام داد. برای مثال تنظیمات خاصی را بارگذاری کرد و یا صفحات ویژه‌ای را فعال و غیرفعال کرد (این موارد را در قسمت‌های بعدی مرور می‌کنیم).
همچنین در اینجا به ازای هر پروفایل مختلف می‌توان Url آغازین یا launch url و پورت آن‌را مجزا درنظر گرفت و یا از وب سرور دیگری استفاده کرد.
مطالب
Google OpenID Authentication در ASP.NET با استفاده از DotNetOpenAuth
در پست‌های قبلی
در این پست می‌خواهیم با یک مثال ساده نشان دهیم که سایت ما چگونه با استفاده از OpenID Authentication می تواند از اکانت گوگل استفاده کرده و کاربر در وب سایت ما شناسایی شود.
برای اینکار ابتدا
1- کتابخانه DotNetOpenAuth را از طریق NuGet به لیست رفرنس‌های پروژه وب خود اضافه نمایید
2- یک صفحه بنام Login.aspx یا هر نام دلخواهی را به پروژه خود اضافه نمایید. در نهایت کد Html صفحه شما به ید به صورت زیر باشد.
<%@ 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>
در کد Html بالا به خصوصیت CommandArgument از کنترل ImageButton دقت نمایید که دارای مقدار "https://www.google.com/accounts/o8/id" می باشد. در واقع این آدرس باید برای اکانت گوگل جهت احراز هویت توسط OpenID استفاده شود. (آدرس API گوگل برای استفاده از این سرویس)
3- در قسمت کد نویسی صفحه کد‌های زیر را وارد نمایید.

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();
        }
    }
}
همانگونه که مشاهده می‌کنید  در رویداد کلیک دکمه لوگین ButtonLoginWithGoogle_Click ابتدا آدرس یکتا گوگل (مقدار CommandArgument اختصاص داده شده به کنترل ImageButton) به همراه آدرس صفحه جاری وب سایت توسط متد CreateRequest از شی openId ترکیب شده و یک درخواست (Request) ساخته شده و در نهایت برای سرویس دهنده گوگل ارسال می‌شود. با اجرای پروژه با تصویر زیر روبرو می‌شوید بروی کلید لوگین کلیک نمایید.

اجرای پروژه

در واقع با کلیک روی دکمه مورد نظر تکه کدی که در بالا شرح داده شد اجرا شده و ما را به صفحه ای شبیه تصویر پایین هدایت می‌کند

رفتن به صفحه گوگل جهت احراز هویت

در صورتی که کلید Sign In انتخاب شود شما به سایت (همین برنامه) اجازه داده اید که از اطلاعات حساب کاربری شما استفاده کند. پس از کلیک به برنامه اصلی (طبق آدرس بازگشت تعیین شده در رویداد ButtonLoginWithGoogle_Click) در رویداد PageLoad صفحه لاگین اطلاعات بازگشتی از سایت سرویس دهنده (در اینجا گوگل) چک می‌شود و با توجه با پاسخ مورد نظر عملیاتی انجام می‌شود، مثلا در صورتی که شما تایید کرده باشید اطلاعات شناسایی شما توسط گوگل در کلیدی به نام GoogleIdentifier در سشن ذهیره شده و شما به صفحه اصلی سایت هدایت می‌شوید.
دموی پروژه در این آدرس قرار داده شده است.
سورس پروژه از این آدرس قابل دسترسی است.
توجه: در این آدرس  شرح داده شده که چگونه دسترسی سایت‌های دیگر به اکانت گوگل خود را قطع کنید
در پست‌های آینده اتصال به تویتر و ف ی س بوک و سایت‌های دیگر توضیح داده خواهد شد.





مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت نهم - مسیریابی
یک برنامه، از صفحات و Viewهای مختلفی تشکیل می‌شود و Routing یا مسیریابی، امکان ناوبری بین این Viewها را میسر می‌کند. یک برنامه‌ی AngularJS 2.0، یک برنامه‌ی تک صفحه‌ای وب است. به این معنا که تمام Viewهای برنامه، در یک صفحه نمایش داده می‌شوند؛ که معمولا همان index.html سایت است. هدف از سیستم مسیریابی، مدیریت نحوه‌ی نمایش و قرارگیری این Viewها، درون تک صفحه‌ی برنامه است.


برپایی تنظیمات اولیه‌ی سیستم مسیریابی در AngularJS 2.0

برای کار با سیستم مسیریابی AngularJS 2.0، ابتدا باید اسکریپت‌های آن به صفحه اضافه شوند. در ادامه المان پایه‌ای تعریف شده و سپس باید سرویس پروایدر مسیریابی را رجیستر کرد. جزئیات این موارد را در ادامه بررسی می‌کنیم:

الف) سرویس مسیریابی، جزئی از angular2/core نیست. به همین جهت مدخل اسکریپت متناظر با آن باید به صفحه‌ی اصلی سایت اضافه شود که این مورد، در قسمت اول بررسی پیشنیازهای نصب AngularJS 2.0 صورت گرفته‌است:
 <!-- Required for routing -->
<script src="~/node_modules/angular2/bundles/router.dev.js"></script>
این تعریف در فایل Views\Shared\_Layout.cshtml (و یا index.html) پروژه‌ی جاری موجود است.

ب) افزودن المان base به ابتدای صفحه:
 <!DOCTYPE html>
<html>
<head>
    <base href="/">
بلافاصله پس از تگ head، المان base اضافه می‌شود. این المان به سیستم مسیریابی اعلام می‌کند که چگونه آدرس‌های خود را تشکیل دهد. به صورت پیش فرض، AngularJS 2.0 از آدرس‌هایی با فرمت HTML5 استفاده می‌کند. در این حالت دیگر نیازی به ذکر # برای مشخص سازی مسیریابی‌های محلی نیست.
از آنجائیکه فایل 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 ...
در اینجا سرویس ROUTER_PROVIDERS به خاصیت providers اضافه شده‌است و همچنین import متناظر با آن نیز به ابتدای صفحه اضافه گردیده‌است.
علت ختم شدن نام این سرویس‌ها به 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>
ب) محتوای فایل product-detail.component.ts
import { Component } from 'angular2/core';

@Component({
    templateUrl: 'app/products/product-detail.component.html'
})
export class ProductDetailComponent  {
    pageTitle: string = 'Product Detail'; 
}
در اینجا یک کامپوننت جدید را ایجاد کرده‌ایم که در قالب آن، مقدار خاصیت pageTitle با روش interpolation در آن درج شده‌است. کلاس ProductDetailComponent، قالب خود را از طریق مقدار دهی آدرس آن در خاصیت templateUrl متادیتای خود، پیدا می‌کند.
اگر دقت کنید، این کامپوننت ویژه دارای خاصیت selector نیست. ذکر selector تنها زمانی اجباری است که بخواهیم این کامپوننت را داخل کامپوننتی دیگر قرار دهیم. در اینجا قصد داریم این کامپوننت را به صورت یک View جدید، توسط سیستم مسیریابی نمایش دهیم و نه به صورت جزئی از یک کامپوننت دیگر.


افزودن تنظیمات مسیریابی به برنامه

مسیریابی در AngularJS 2.0، مبتنی بر کامپوننت‌ها است. به همین جهت، ابتدای کار مسیریابی، مشخص سازی تعدادی از کامپوننت‌ها هستند که قرار است به عنوان مقصد سیستم راهبری (navigation) مورد استفاده قرار گیرند و به ازای هر کدام، یک مسیریابی و Route جدید را تعریف می‌کنیم. تعریف هر Route جدید شامل انتساب نامی به آن، تعیین مسیر مدنظر و مشخص سازی کامپوننت مرتبط است:
 { path: '/products', name: 'Products', component: ProductListComponent },
برای مثال جهت تعریف Route کامپوننت لیست محصولات، نام آن‌را Products، مسیر آن‌را products/ و در آخر کامپوننت آن‌را به نام کلاس متناظر با آن، تنظیم می‌کنیم.
این تنظیمات به عنوان یک متادیتای جدید دیگر به کلاس 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 به کلاس AppComponent اضافه شده‌است و همچنین importهای متناظری نیز در ابتدای این فایل تعریف شده‌اند.
همانطور که ملاحظه می‌کنید، یک کلاس می‌تواند بیش از یک 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";
}
ب) محتوای فایل welcome.component.html
<div class="panel panel-primary">
    <div class="panel-heading">
        {{pageTitle}}
    </div>
    <div class="panel-body">
        <h3 class="text-center">Default page</h3>
    </div>
</div>
کار این کامپوننت، نمایش صفحه‌ی آغازین برنامه است؛ بر اساس تنظیم useAsDefault: true مسیریابی‌های تعریف شده‌.
پس از تعریف این کامپوننت، اکنون باید 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 …
})
در اینجا یک navigation bar بوت استرپ 3، جهت تعریف منوی بالای صفحه اضافه شده‌است.
سپس جهت تعریف لینک‌های هر آیتم، از یک دایرکتیو توکار 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],
علت جمع بود نام این دایرکتیو این است که routerLink استفاده شده، یکی از اعضای مجموعه‌ی دایرکتیوهای مسیریابی توکار AngularJS 2.0 است.

تا اینجا اگر دقت کرده باشید، کامپوننت نمایش لیست محصولات را از کامپوننت ریشه‌ی سایت حذف کرده‌ایم و بجای آن منوی بالای سایت را نمایش می‌دهیم که توسط آن می‌توان به صفحه‌ی آغازین و یا صفحه‌ی نمایش لیست محصولات، رسید. به همین جهت خاصیت 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 …
ابتدا مسیریابی جدیدی به نام ProductDetail اضافه شده‌است و سپس ماژول دربرگیرنده‌ی کلاس کامپوننت آن نیز import شده‌است.
تفاوت این مسیریابی با نمونه‌های قبلی در تعریف id:/ است. پس از ذکر :/، نام یک متغیر عنوان می‌شود و اگر نیاز به چندین متغیر بود، همین الگو را تکرار خواهیم کرد.

ب) سپس نحوه‌ی فعال سازی این مسیریابی را توسط تعریف لینکی جدید، معرفی می‌کنیم. بنابراین فایل قالب product-list.component.html را گشوده و سپس بجای نمایش عنوان محصول:
 <td>{{ product.productName }}</td>
لینک به جزئیات آن‌را نمایش می‌دهیم:
<td>
    <a [routerLink]="['ProductDetail', {id: product.productId}]">
        {{product.productName}}
    </a>
</td>
نحوه‌ی تعریف این لینک، با لینک‌هایی که در منوی بالای سایت اضافه کردیم، یکی است؛ با این تفاوت که اکنون پارامتر دومی را به قسمت یافتن نام این Route، جهت مشخص سازی روش مقدار دهی متغیر id، تعریف کرده‌ایم. در اینجا id هر لینک از productId بایند شده تامین می‌شود.
اکنون که از دایرکتیو جدید 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}`;
    } 
}
با نحوه‌ی تزریق وابستگی‌ها در قسمت هفتم آشنا شدیم. در اینجا سرویس توکار RouteParams به سازنده‌ی کلاس تزریق شده‌‌است. با استفاده از آن می‌توان به id ارسالی از طریق url دسترسی یافت. در اینجا پارامتری که به متد get ارسال می‌شود، باید با نام پارامتری که در تنظیمات آغازین مسیریابی سیستم تعریف گردید، تطابق داشته باشد.
در این حالت، 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>
در اینجا دکمه‌ی بازگشت به صفحه‌ی قبلی اضافه شده‌است که به متد onBack در کلاس مرتبط با این قالب، متصل است.

سپس کدهای 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']);
    }
}
در اینجا سرویس جدیدی به نام Router در سازنده‌ی کلاس تزریق شده‌است. این سرویس امکان فراخوانی متدهایی مانند navigate را جهت حرکت به مسیریابی مشخصی، میسر می‌کند. پارامتری که به این متد ارسال می‌شود، دقیقا معادل همان پارامتری است که به دایرکتیو routerLink ارسال می‌گردد و در اینجا صرفا نام یک مسیریابی مشخص شده‌است؛ بدون ذکر پارامتری.


رفع تداخل مسیریابی‌های 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; }
}
تفاوتی نمی‌کند که از چه منبع داده‌ای استفاده می‌کنید. آیا قرار است یک سری فایل متنی ساده موجود در یک پوشه را ایندکس کنید یا تعدادی رکورد بانک اطلاعاتی؛ از NHibernate استفاده می‌کنید یا از Entity framework و یا از ADO.NET. کتابخانه Lucene مستقل است از منبع داده مورد استفاده و تنها اطلاعاتی با فرمت شیء Document معرفی شده به آن‌را می‌شناسد.


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;
}
این متد وهله‌ای از شیء Post را دریافت کرده و آن‌را تبدیل به یک سند Lucene می‌کند.
کار با ایجاد یک وهله از شیء 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();
    }
}
ابتدا محل ذخیره سازی فایل‌های full text search مشخص می‌شوند. سپس آنالیز کننده اطلاعات باید معرفی شود. در ادامه به کمک این اطلاعات، شیء IndexWriter ایجاد و مستندات لوسین به آن اضافه می‌شوند. در آخر، این اطلاعات بهینه سازی شده و ثبت نهایی صورت خواهد گرفت.
ذکر 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();
        }
}
تنها نکته مهم این متدها، استفاده از متد IndexWriter با پارامتر create مساوی false است. به این ترتیب فایل‌های موجود بجای از نو ساخته شدن، به روز خواهند شد.
محل فراخوانی این متدها هم می‌تواند در کنار متدهای به روز رسانی اطلاعات اصلی در بانک اطلاعاتی برنامه باشند. اگر رکوردی اضافه یا حذف شده، ایندکس متناظر نیز باید به روز شود.


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، ضخیم نمایش داده خواهند شد.