مطالب دوره‌ها
استفاده از StructureMap جهت تزریق وابستگی‌ها در برنامه‌های WPF و الگوی MVVM
در این قسمت قصد داریم همانند کنترلرها در ASP.NET MVC، کار تزریق وابستگی‌ها را در متدهای سازنده ViewModelهای WPF بدون استفاده از الگوی Service locator انجام دهیم؛ برای مثال:
    public class TestViewModel
    {
        private readonly ITestService _testService;
        public TestViewModel(ITestService testService) //تزریق وابستگی در سازنده کلاس
        {
            _testService = testService;
        }
و همچنین کار اتصال یک ViewModel، به View متناظر آن‌را نیز خودکار کنیم. قراردادی را نیز در اینجا بکار خواهیم گرفت:
نام تمام Viewهای برنامه به View ختم می‌شوند و نام ViewModelها به ViewModel. برای مثال TestViewModel و TestView معرف یک ViewModel و View متناظر خواهند بود.


ساختار کلاس‌های لایه سرویس برنامه

namespace DI07.Services
{
    public interface ITestService
    {
        string Test();
    }
}

namespace DI07.Services
{
    public class TestService: ITestService
    {
        public string Test()
        {
            return "برای آزمایش";
        }
    }
}
یک پروژه WPF را آغاز کرده و سپس یک پروژه Class library دیگر را به نام Services با دو کلاس و اینترفیس فوق، به آن اضافه کنید. هدف از این کلاس‌ها صرفا آشنایی با نحوه تزریق وابستگی‌ها در سازنده یک کلاس ViewModel در WPF است.


علامتگذاری ViewModelها

در ادامه یک اینترفیس خالی را به نام IViewModel مشاهده می‌کنید:
namespace DI07.Core
{
    public interface IViewModel // از این اینترفیس خالی برای یافتن و علامتگذاری ویوو مدل‌ها استفاده می‌کنیم
    {
    }
}
از این اینترفیس برای علامتگذاری ViewModelهای برنامه استفاده خواهد شد. این روش، یکی از انواع روش‌هایی است که در مباحث Reflection برای یافتن کلاس‌هایی از نوع مشخص استفاده می‌شود.
برای نمونه کلاس TestViewModel برنامه، با پیاده سازی IViewModel، به نوعی نشانه گذاری نیز شده است:
using DI07.Services;
using DI07.Core;

namespace DI07.ViewModels
{
    public class TestViewModel : IViewModel // علامتگذاری ویوو مدل
    {
        private readonly ITestService _testService;
        public TestViewModel(ITestService testService) //تزریق وابستگی در سازنده کلاس
        {
            _testService = testService;
        }

        public string Data
        {
            get { return _testService.Test(); }
        }
    }
}


تنظیمات آغازین IoC Container مورد استفاده

در کلاس استاندارد App برنامه WPF خود، کار تنظیمات اولیه StructureMap را انجام خواهیم داد:
using System.Windows;
using DI07.Core;
using DI07.Services;
using StructureMap;

namespace DI07
{
    public partial class App
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            ObjectFactory.Configure(cfg =>
            {
                cfg.For<ITestService>().Use<TestService>();

                cfg.Scan(scan =>
                {
                    scan.TheCallingAssembly();
                    // Add all types that implement IView into the container, 
                    // and name each specific type by the short type name.
                    scan.AddAllTypesOf<IViewModel>().NameBy(type => type.Name);
                    scan.WithDefaultConventions();
                });
            });
        }
    }
}
در اینجا عنوان شده است که اگر نیاز به نوع ITestService وجود داشت، کلاس TestService را وهله سازی کن.
همچنین در ادامه از قابلیت اسکن این IoC Container برای یافتن کلاس‌هایی که IViewModel را در اسمبلی جاری پیاده سازی کرده‌اند، استفاده شده است. متد NameBy، سبب می‌شود که بتوان به این نوع‌های یافت شده از طریق نام کلاس‌های متناظر دسترسی یافت.


اتصال خودکار ViewModelها به Viewهای برنامه

using System.Windows.Controls;
using StructureMap;

namespace DI07.Core
{
    /// <summary>
    /// Stitches together a view and its view-model
    /// </summary>
    public static class ViewModelFactory
    {
        public static void WireUp(this ContentControl control)
        {
            var viewName = control.GetType().Name;
            var viewModelName = string.Concat(viewName, "Model"); //قرار داد نامگذاری ما است
            control.Loaded += (s, e) =>
            {
                control.DataContext = ObjectFactory.GetNamedInstance<IViewModel>(viewModelName);
            };
        }
    }
}
اکنون که کار علامتگذاری ViewModelها انجام شده و همچنین IoC Container ما می‌داند که چگونه باید آن‌ها را در اسمبلی جاری جستجو کند، مرحله بعدی، ایجاد کلاسی است که از این تنظیمات استفاده می‌کند. در کلاس ViewModelFactory، متد WireUp، وهله‌ای از یک View را دریافت کرده، نام آن‌را استخراج می‌کند و سپس بر اساس قراردادی که در ابتدای بحث وضع کردیم، نام ViewModel متناظر را یافته و سپس زمانیکه این View بارگذاری می‌شود، به صورت خودکار DataContext آن‌را به کمک StructureMap وهله سازی می‌کند. این وهله سازی به همراه تزریق خودکار وابستگی‌ها در سازنده کلاس ViewModel نیز خواهد بود.


استفاده از کلاس ViewModelFactory

در ادامه کدهای TestView و پنجره اصلی برنامه را مشاهده می‌کنید:

<UserControl x:Class="DI07.Views.TestView"
             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" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <TextBlock Text="{Binding Data}" />
    </Grid>
</UserControl>


<Window x:Class="DI07.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Views="clr-namespace:DI07.Views"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Views:TestView />
    </Grid>
</Window>
در فایل Code behind مرتبط با TestView تنها کافی است سطر فراخوانی this.WireUp اضافه شود تا کار تزریق وابستگی‌ها، وهله سازی ViewModel متناظر و همچنین مقدار دهی DataContext آن به صورت خودکار انجام شود:
using DI07.Core;

namespace DI07.Views
{
    public partial class TestView
    {
        public TestView()
        {
            InitializeComponent();
            this.WireUp(); //تزریق خودکار وابستگی‌ها و یافتن ویوو مدل متناظر
        }
    }
}

دریافت پروژه کامل این قسمت
  DI07.zip
مطالب
تکمیل کلاس DelegateCommand

مدت‌ها از کلاس DelegateCommand معرفی شده در این آدرس استفاده می‌کردم. این کلاس یک مشکل جزئی دارد و آن هم عدم بررسی مجدد قسمت canExecute به صورت خودکار هست.

خلاصه‌ای برای کسانی که بار اول هست با این مباحث برخورد می‌کنند؛ یا MVVM به زبان بسیار ساده:

در برنامه نویسی متداول سیستم مایکروسافتی، در هر سیستمی که ایجاد کرده و در هر فناوری که ارائه داده از زمان VB6 تا امروز، شما روی یک دکمه مثلا دوبار کلیک می‌کنید و در فایل اصطلاحا code behind این فرم و در روال رخدادگردان آن شروع به کد نویسی خواهید کرد. این مورد تقریبا در همه جا صادق است؛ از WinForms تا WPF تا Silverlight تا حتی ASP.NET Webforms . به عمد هم این طراحی صورت گرفته تا برنامه نویس‌ها در این محیط‌ها زیاد احساس غریبی نکنند. اما این روش یک مشکل مهم دارد و آن هم «توهم» جداسازی رابط کاربر از کدهای برنامه است. به ظاهر یک فایل فرم وجود دارد و یک فایل جدای code behind ؛ اما در عمل هر دوی این‌ها یک partial class یا به عبارتی «یک کلاس» بیشتر نیستند. «فکر می‌کنیم» که از هم جدا شدند اما واقعا یکی هستند. شما در code behind صفحه به صورت مستقیم با عناصر رابط کاربری سروکار دارید و کدهای شما به این عناصر گره خورده‌اند.
شاید بپرسید که چه اهمیتی دارد؟
مشکل اول: امکان نوشتن آزمون‌ها واحد برای این متدها وجود ندارد یا بسیار سخت است. این متدها فقط با وجود فرم و رابط کاربری متناظر با آن‌ها هست که معنا پیدا می‌کنند و تک تک عناصر آن‌ها وهله سازی می‌شوند.
مشکل دوم: کد نوشته فقط برای همین فرم جاری آن قابل استفاده است؛ چون به صورت صریح به عناصر موجود در فرم اشاره می‌کند. نمی‌تونید این فایل code behind رو بردارید بدون هیچ تغییری برای فرم دیگری استفاده کنید.
مشکل سوم: نمی‌تونید طراحی فرم رو بدید به یک نفر، کد نویسی اون رو به شخصی دیگر. چون ایندو لازم و ملزوم یکدیگرند.

این سیستم کد نویسی دهه 90 است.
چند سالی است که طراحان سعی کرده‌اند این سیستم رو دور بزنند و روش‌هایی رو ارائه بدن که در آن‌ها فرم‌های برنامه و فایل‌های پیاده سازی کننده‌ی منطق آن هیچگونه ارتباط مستقیمی باهم نداشته باشند؛ به هم گره نخورده باشند؛ ارجاعی به هیچیک از عناصر بصری فرم را در خود نداشته باشند. به همین دلیل ASP.NET MVC به وجود آمده و در همان سال‌ها مثلا MVVM .

سؤال:
الان که رابط کاربری از فایل پیاده سازی کننده منطق آن جدا شده و دیگر Code behind هم نیست (همان partial class های متداول)، این فایل‌ها چطور متوجه می‌شوند که مثلا روی یک فرم، شیءایی قرار گرفته؟ از کجا متوجه خواهند شد که روی دکمه‌ای کلیک شده؟ این‌ها که ارجاعی از فرم را در درون خود ندارند.
در الگوی MVVM این سیم کشی توسط امکانات قوی Binding موجود در WPF میسر می‌شود. در ASP.NET MVC چیزی شبیه به آن به نام Model binder و همان مکانیزم‌های استاندارد HTTP این کار رو می‌کنه. در MVVM شما بجای code behind خواهید داشت ViewModel (اسم جدید آن). در ASP.NET MVC این اسم شده Controller. بنابراین اگر این اسامی رو شنیدید زیاد تعجب نکنید. این‌ها همان Code behind قدیمی هستند اما ... بدون داشتن ارجاعی از رابط کاربری در خود که ... اطلاعات موجود در فرم به نحوی به آن‌ها Bind و ارسال می‌شوند.
این سیم کشی‌ها هم نامرئی هستند. یعنی فایل ViewModel یا فایل Controller نمی‌دونند که دقیقا از چه کنترلی در چه فرمی این اطلاعات دریافت شده.
این ایده هم جدید نیست. شاید بد نباشه به دوران طلایی Win32 برگردیم. همان توابع معروف PostMessage و SendMessage را به خاطر دارید؟ شما در یک ترد می‌تونید با مثلا PostMessage شیءایی رو به یک فرم که در حال گوش فرا دادن به تغییرات است ارسال کنید (این سیم کشی هم نامرئی است). بنابراین پیاده سازی این الگوها حتی در Win32 و کلیه فریم ورک‌های ساخته شده بر پایه آن‌ها مانند VCL ، VB6 ، WinForms و غیره ... «از روز اول» وجود داشته و می‌تونستند بعد از 10 سال نیان بگن که اون روش‌های RAD ایی رو که ما پیشنهاد دادیم، می‌شد خیلی بهتر از همان ابتدا، طور دیگری پیاده سازی بشه.

ادامه بحث!
این سیم کشی یا اصطلاحا Binding ، در مورد رخدادها هم در WPF وجود داره و اینبار به نام Commands معرفی شده‌است. به این معنا که بجای اینکه بنویسید:
<Button  Click="btnClick_Event">Last</Button>

بنویسید:
<Button Command="{Binding GoLast}">Last</Button>

حالا باید مکانیزمی وجود داشته باشه تا این پیغام رو به ViewModel برنامه برساند. اینکار با پیاده سازی اینترفیس ICommand قابل انجام است که معرفی یک کلاس عمومی از پیاده سازی آن‌را در ابتدای بحث مشاهده نمودید.
در یک DelegateCommand،‌ توسط متد منتسب به executeAction، مشخص خواهیم کرد که اگر این سیم کشی برقرار شد (که ما دقیقا نمی‌دانیم و نمی‌خواهیم که بدانیم از کجا و کدام فرم دقیقا)، لطفا این اعمال را انجام بده و توسط متد منتسب به canExecute به سیستم Binding خواهیم گفت که آیا مجاز هستی این اعمال را انجام دهی یا خیر. اگر این متد false برگرداند، مثلا دکمه یاد شده به صورت خودکار غیرفعال می‌شود.
اما مشکل کلاس DelegateCommand ذکر شده هم دقیقا همینجا است. این دکمه تا ابد غیرفعال خواهد ماند. در WPF کلاسی وجود دارد به نام CommandManager که حاوی متدی استاتیکی است به نام InvalidateRequerySuggested. اگر این متد به صورت دستی فراخوانی شود، یکبار دیگر کلیه متدهای منتسب به تمام canExecute های تعریف شده، به صورت خودکار اجرا می‌شوند و اینجا است که می‌توان دکمه‌ای را که باید مجددا بر اساس شرایط جاری تغییر وضعیت پیدا کند، فعال کرد. بنابراین فراخوانی متد InvalidateRequerySuggested یک راه حل کلی رفع نقیصه‌ی ذکر شده است.
راه حل دومی هم برای حل این مشکل وجود دارد. می‌توان از رخدادگردان CommandManager.RequerySuggested استفاده کرد. روال منتسب به این رخدادگردان هر زمانی که احساس کند تغییری در UI رخ داده، فراخوانی می‌شود. بنابراین پیاده سازی بهبود یافته کلاس DelegateCommand به صورت زیر خواهد بود:

using System;
using System.Windows.Input;

namespace MvvmHelpers
{
// Ref.
// - http://johnpapa.net/silverlight/5-simple-steps-to-commanding-in-silverlight/
// - http://joshsmithonwpf.wordpress.com/2008/06/17/allowing-commandmanager-to-query-your-icommand-objects/
public class DelegateCommand<T> : ICommand
{
readonly Func<T, bool> _canExecute;
bool _canExecuteCache;
readonly Action<T> _executeAction;

public DelegateCommand(Action<T> executeAction, Func<T, bool> canExecute = null)
{
if (executeAction == null)
throw new ArgumentNullException("executeAction");

_executeAction = executeAction;
_canExecute = canExecute;
}

public event EventHandler CanExecuteChanged
{
add { if (_canExecute != null) CommandManager.RequerySuggested += value; }
remove { if (_canExecute != null) CommandManager.RequerySuggested -= value; }
}

public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute((T)parameter);
}

public void Execute(object parameter)
{
_executeAction((T)parameter);
}
}
}

استفاده از آن هم در ViewModel ساده است. یکبار خاصیتی به این نام تعریف می‌شود. سپس در سازنده کلاس مقدار دهی شده و متدهای متناظر آن تعریف خواهند شد:

public DelegateCommand<string> GoLast { set; get; }

//in ctor
GoLast = new DelegateCommand<string>(goLast, canGoLast);

private bool canGoLast(string data)
{
//ex.
return ListViewGuiData.CurrentPage != ListViewGuiData.TotalPage - 1;
}

private void goLast(string data)
{
//do something
}

مزیت کلاس DelegateCommand جدید هم این است که مثلا متد canGoLast فوق، به صورت خودکار با به روز رسانی UI ، فراخوانی و تعیین اعتبار مجدد می‌شود.


مطالب
MVVM و نمایش دیالوگ‌ها

بسیاری از برنامه‌های دسکتاپ نیاز به نمایش پنجره‌های دیالوگ استاندارد ویندوز مانند OpenFileDialog و SaveFileDialog را دارند و سؤال اینجا است که چگونه اینگونه موارد را باید از طریق پیاده سازی صحیح الگوی MVVM مدیریت کرد؛ از آنجائیکه خیلی راحت در فایل ViewModel می‌توان نوشت new OpenFileDialog و الی آخر. این مورد هم یکی از دلایل اصلی استفاده از الگوی MVVM را زیر سؤال می‌برد : این ViewModel دیگر قابل تست نخواهد بود. همیشه شرایط آزمون‌های واحد را به این صورت در نظر بگیرید:
سروری وجود دارد در جایی که به آن دسترسی نداریم. روی این سرور با اتوماسیونی که راه انداخته‌ایم، آخر هر روز آزمون‌های واحد موجود به صورت خودکار انجام شده و یک گزارش تهیه می‌شود (مثلا یک نوع continuous integration سرور). بنابراین کسی دسترسی به سرور نخواهد داشت تا این OpenFileDialog ظاهر شده را مدیریت کرده، فایلی را انتخاب و به برنامه آزمون واحد معرفی کند. به صورت خلاصه ظاهر شدن هر نوع دیالوگی حین انجام آزمون‌های واحد «مسخره» است!
یکی از روش‌های حل این نوع مسایل، استفاده از dependency injection یا تزریق وابستگی‌ها است و در ادامه خواهیم دید که چگونه WPF‌ بدون نیاز به هیچ نوع فریم ورک تزریق وابستگی خارجی، از این مفهوم پشتیبانی می‌کند.

مروری مقدماتی بر تزریق وابستگی‌ها
امکان نوشتن آزمون واحد برای new OpenFileDialog وجود ندارد؟ اشکالی نداره، یک Interface بر اساس نیاز نهایی برنامه درست کنید (نیاز نهایی برنامه از این ماجرا فقط یک رشته LoadPath است و بس) سپس در ViewModel با این اینترفیس کار کنید؛ چون به این ترتیب امکان «تقلید» آن فراهم می‌شود.

یک مثال عملی:
ViewModel نیاز دارد تا مسیر فایلی را از کاربر بپرسد. این مساله را با کمک dependency injection در ادامه حل خواهیم کرد.
ابتدا سورس کامل این مثال:

ViewModel برنامه (تعریف شده در پوشه ViewModels برنامه):

namespace WpfFileDialogMvvm.ViewModels
{
public interface IFilePathContract
{
string GetFilePath();
}

public class MainWindowViewModel
{
IFilePathContract _filePathContract;
public MainWindowViewModel(IFilePathContract filePathContract)
{
_filePathContract = filePathContract;
}

//...

private void load()
{
string loadFilePath = _filePathContract.GetFilePath();
if (!string.IsNullOrWhiteSpace(loadFilePath))
{
// Do something
}
}
}
}

دو نمونه از پیاده سازی اینترفیس IFilePathContract تعریف شده (در پوشه Dialogs برنامه):

using Microsoft.Win32;
using WpfFileDialogMvvm.ViewModels;

namespace WpfFileDialogMvvm.Dialogs
{
public class OpenFileDialogProvider : IFilePathContract
{
public string GetFilePath()
{
var ofd = new OpenFileDialog
{
Filter = "XML files (*.xml)|*.xml"
};
string filePath = null;
bool? dialogResult = ofd.ShowDialog();
if (dialogResult.HasValue && dialogResult.Value)
{
filePath = ofd.FileName;
}
return filePath;
}
}

public class FakeOpenFileDialogProvider : IFilePathContract
{
public string GetFilePath()
{
return @"c:\path\data.xml";
}
}
}

و View برنامه:

<Window x:Class="WpfFileDialogMvvm.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:WpfFileDialogMvvm.ViewModels"
xmlns:dialogs="clr-namespace:WpfFileDialogMvvm.Dialogs"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<ObjectDataProvider x:Key="mainWindowViewModel"
ObjectType="{x:Type vm:MainWindowViewModel}">
<ObjectDataProvider.ConstructorParameters>
<dialogs:OpenFileDialogProvider/>
</ObjectDataProvider.ConstructorParameters>
</ObjectDataProvider>
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource mainWindowViewModel}}">

</Grid>
</Window>

توضیحات:
ما در ViewModel نیاز داریم تا مسیر نهایی فایل را دریافت کنیم و این عملیات نیاز به فراخوانی متد ShowDialog ایی را دارد که امکان نوشتن آزمون واحد خودکار را از ViewModel ما سلب خواهد کرد. بنابراین بر اساس نیاز برنامه یک اینترفیس عمومی به نام IFilePathContract را طراحی می‌کنیم. در حالت کلی کلاسی که این اینترفیس را پیاده سازی می‌کند، قرار است مسیری را برگرداند. اما به کمک استفاده از اینترفیس، به صورت ضمنی اعلام می‌کنیم که «برای ما مهم نیست که چگونه». می‌خواهد OpenFileDialogProvider ذکر شده باشد، یا نمونه تقلیدی مانند FakeOpenFileDialogProvider. از نمونه واقعی OpenFileDialogProvider در برنامه اصلی استفاده خواهیم کرد، از نمونه تقلیدی FakeOpenFileDialogProvider در آزمون واحد و نکته مهم هم اینجا است که ViewModel ما چون بر اساس اینترفیس IFilePathContract پیاده سازی شده، با هر دو DialogProvider یاد شده می‌تواند کار کند.
مرحله آخر نوبت به وهله سازی نمونه واقعی، در View برنامه است. یا می‌توان در Code behind مرتبط با View نوشت:

namespace WpfFileDialogMvvm
{
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel(new OpenFileDialogProvider());
}
}
}

و یا از روش ObjectDataProvider توکار WPF هم می‌شود استفاده کرد؛ که مثال آن‌را در کدهای XAML مرتبط با View ذکر شده می‌توانید مشاهده کنید. ابتدا دو فضای نام vm و dialog تعریف شده (با توجه به اینکه مثلا در این مثال، دو پوشه ViewModels و Dialogs وجود دارند). سپس کار تزریق وابستگی‌ها به سازنده کلاس MainWindowViewModel،‌ از طریق ObjectDataProvider.ConstructorParameters انجام می‌شود:

<ObjectDataProvider x:Key="mainWindowViewModel" 
ObjectType="{x:Type vm:MainWindowViewModel}">
<ObjectDataProvider.ConstructorParameters>
<dialogs:OpenFileDialogProvider/>
</ObjectDataProvider.ConstructorParameters>
</ObjectDataProvider>

مطالب
نحوه نمایش تمام آیکون‌های تعریف شده در یک قلم در WPF
سال نو مبارک! به امید روزهایی شاد، سلامت و پر برکت.

پیرو مطلب قلم‌هایی حاوی آیکون که خصوصا در برنامه‌های مترو بیشتر مرسوم شده‌اند، شاید بد نباشد کار برنامه Character Map ویندوز را با WPF شبیه سازی کنیم.
ابتدا Model و ViewModel  این برنامه را درنظر بگیرید:
namespace CrMap.Models
{
    public class Symbol
    {
        public char Character { set; get; }
        public string CharacterCode { set; get; }
    }
}

using System;
using System.Collections.Generic;
using System.Windows.Media;
using CrMap.Models;

namespace CrMap.ViewModels
{
    public class CrMapViewModel
    {
        public IList<Symbol> Symbols { set; get; }
        public int GridRows { set; get; }
        public int GridCols { set; get; }

        public CrMapViewModel()
        {
            fillDataSource();
        }

        private void fillDataSource()
        {
            Symbols = new List<Symbol>();
            GridCols = 15;

            var fontFamily = new FontFamily(new Uri("pack://application:,,,/"), "/Fonts/#whhglyphs");

            GlyphTypeface glyph = null;
            foreach (var typeface in fontFamily.GetTypefaces())
            {
                if (typeface.TryGetGlyphTypeface(out glyph) && (glyph != null))
                    break;
            }

            if (glyph == null)
                throw new InvalidOperationException("Couldn't find a GlyphTypeface.");

            GridRows = (glyph.CharacterToGlyphMap.Count / GridCols) + 1;

            foreach (var item in glyph.CharacterToGlyphMap)
            {
                var index = item.Key;
                Symbols.Add(new Symbol
                {
                    Character = Convert.ToChar(index),
                    CharacterCode = string.Format("&#x{0:X}", index)
                });
            }
        }
    }
}
توضیحات:

یک سری قابلیت جالب در WPF برای استخراج اطلاعات قلم‌ها وجود دارند که در فضای نام System.Windows.Media اسمبلی PresentationCore.dll قرار گرفته‌اند. برای نمونه پس از وهله سازی FontFamily بر اساس یک قلم مدفون شده در برنامه، امکان استخراج تعداد گلیف‌های موجود در این قلم وجود دارد که نحوه انجام آن‌را در متد fillDataSource ملاحظه می‌کنید.
این اطلاعات استخراج شده و لیست Symbols برنامه را تشکیل می‌دهند. در نهایت برای نمایش این اطلاعات، از ترکیب ItemsControl و UniformGrid استفاده خواهیم کرد:
<Window x:Class="CrMap.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:CrMap.ViewModels"
        Title="MainWindow" WindowStartupLocation="CenterScreen" WindowState="Maximized"
        Height="350" Width="525">
    <Window.Resources>
        <vm:CrMapViewModel x:Key="vmCrMapViewModel" />
    </Window.Resources>
    <ScrollViewer VerticalScrollBarVisibility="Visible">
        <ItemsControl
                      DataContext="{StaticResource vmCrMapViewModel}"  
                      ItemsSource="{Binding Symbols}"
                      Name="MainItemsControl"
                      VerticalAlignment="Top"     
                      HorizontalAlignment="Center"
                      Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="4">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        Columns="{Binding GridCols}" 
                        Rows="{Binding GridRows}">
                    </UniformGrid>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <ContentControl>
                        <Border BorderBrush="SlateGray"
                            HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                            BorderThickness="1" CornerRadius="3" Margin="3">
                            <StackPanel Margin="3" Orientation="Vertical">
                                <TextBlock                                         
                                    HorizontalAlignment="Center" 
                                    VerticalAlignment="Center" 
                                    FontFamily="Fonts/#whhglyphs"
                                    Foreground="DarkRed"
                                    FontSize="17"
                                    Text="{Binding Character}" />
                                <TextBlock                                         
                                    HorizontalAlignment="Center" 
                                    VerticalAlignment="Center" 
                                    Text="{Binding CharacterCode}" />
                            </StackPanel>
                        </Border>
                    </ContentControl>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </ScrollViewer>
</Window>


دریافت مثال این مطلب
CrMap.zip
 
مطالب
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 صورت می‌گیرد. 

مطالب
ساخت فرم های generic در WPF و Windows Application
آیا می‌توان در یک پروژه های  Windows App یا WPF، یک فرم پایه به صورت generic تعریف کنیم و سایر فرم‌ها بتوانند از آن ارث ببرند؟ در این پست به تشریح و بررسی این مسئله  خواهیم پرداخت.
در پروژه هایی به صورت Smart UI کد نویسی شده اند و یا حتی قصد انجام پروژه با تکنولوژی‌های WPF یا Windows Application را دارید و نیاز دارید که فرم‌های خود را به صورت generic بسازید این مقاله به شما کمک خواهد کرد.
#Windows Application
یک پروژه از نوع Windows Application ایجاد می‌کنیم و یک فرم به نام FrmBase در آن خواهیم داشت. یک Label در فرم قرار دهید  و مقدار Text آن را فرم اصلی قرار دهید.
در فرم مربوطه، فرم را به صورت generic تعریف کنید. به صورت زیر:
public partial class FrmBase<T> : Form where T : class 
    {
        public FrmBase()
        {
            InitializeComponent();
        }
    }

بعد باید همین تغییرات را در فایل FrmBase.designer.cs هم اعمال کنیم:
partial class FrmBase<T> where T : class 
    {
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose( bool disposing )
        {
            if ( disposing && ( components != null ) )
            {
                components.Dispose();
            }
            base.Dispose( disposing );
        }

        #region Windows Form Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.label1 = new System.Windows.Forms.Label();
            this.SuspendLayout();
            // 
            // label1
            // 
            this.label1.AutoSize = true;
            this.label1.Location = new System.Drawing.Point(186, 22);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(51, 13);
            this.label1.TabIndex = 0;
            this.label1.Text = "فرم اصلی";
            // 
            // FrmBase
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(445, 262);
            this.Controls.Add(this.label1);
            this.Name = "FrmBase";
            this.Text = "Form1";
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private System.Windows.Forms.Label label1;
    }
یک فرم جدید بسازید و نام آن را FrmTest بگذارید. این فرم باید از FrmBase ارث ببرد. خب این کار را به صورت زیر انجام می‌دهیم:
public partial class FrmTest : FrmBase<String>
    {
        public FrmTest()
        {
            InitializeComponent();
        }
    }
پروژه را اجرا کنید. بدون هیچ گونه مشکلی برنامه اجرا می‌شود وفرم مربوطه را در حالت اجرا مشاهده خواهید کرد. اما اگر قصد باز کردن فرم FrmTest را در حالت design داشته باشید با خطای زیر مواجه خواهید شد:

با این که برنامه به راحتی اجرا می‌شود و خروجی آن قابل مشاهده است ولی امکان نمایش فرم در حالت design وجود ندارد. متاسفانه در Windows App‌ها برای تعریف فرم‌ها به صورت generic یا این مشکل روبرور هستیم. تنها راه موجود برای حل این مشکل استفاده از یک کلاس کمکی است. به صورت زیر:

    public partial class FrmTest : FrmTestHelp
    {
        public FrmTest()
        {
            InitializeComponent();
        }
    }

    public class FrmTestHelp : FrmBase<String>
    {
    }
مشاهده می‌کنید که بعد از اعمال تغییرات بالا فرم FrmTest به راحتی Load می‌شود و در حالت designer هم می‌تونید از آن استفاده کنید.
#WPF
در پروژه‌های WPF ، راه حلی برای این مشکل در نظر گرفته شده است. در WPF، برای Window یا UserControl پایه نمی‌توان Designer داشت. ابتدا باید فرم پایه را به صورت زیر ایجاد کنیم:
public class WindowBase<T> : Window where T : class
{
}
در این مرحله یک Window بسازید که از WindowBase ارث ببرد:
public partial class MainWindow: WindowBase<String>
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
در WPF باید تعاریف موجود برای Xaml و Code Behind یکی باشد. در نتیجه باید تغییرات زیر را در فایل Xaml نیز اعمال کنید:
<local:WindowBase x:Class="GenericWindows.MainWindow" 
x:TypeArguments="sys:String" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:GenericWindows" xmlns:sys="clr-namespace:System;assembly=mscorlib" Title="MainWindow" Height="350" Width="525"> <Grid> </Grid> </local:WindowBase>
همان طور که می‌بیینید در ابتدای فایل به جای Window از local:WindowBase استفاده شده است. این نشان دهنده این است که فرم پایه برای این Window از نوع WindowBase است. برای مشخص کردن نوع generic هم می‌تونید از x:TypeArguments استفاده کنید که در این جا نوع آن را String انتخاب کردم.
مطالب
بارگذاری UserControl در WPF به کمک الگوی MVVM
در نرم افزارهای تحت ویندوز روشها و سلیقه‌های متفاوتی برای چینش فرمها ، منو‌ها و دیگر اجزای برنامه وجود دارد. در یک نرم افزار اتوماسیون اداری که فرمهای ورود اطلاعات زیادی دارد فضای کافی برای نمایش همه‌ی فرم‌ها به کاربر نیست. یکی از روش هایی که می‌تواند به کار رود تقسیم قسمت‌های مختلف نرم افزار در View‌های جداگانه است. این کار استفاده‌ی مجدد از قسمت‌های مختلف و نگهداری کد را سهولت می‌بخشد. 

الگوی متداولی که در نرم افزار‌های WPF و Silverlight استفاده می‌شود الگوی MVVM است. (این الگو در جاوااسکریپت هم به سبب Statefull بودن استفاده می‌شود.) قبلا مطالب زیادی در این سایت جهت آموزش و توضیح این الگوی منتشر شده است.
فرض کنید نرم افزار از چند بخش تشکیل شده :

  • صفحه‌ی اصلی
  • منو
  • یک صفحه‌ی خوش آمدگویی
  • صفحه‌ی ورود و نمایش اطلاعات
می توان اجزا و تعریف هر یک از این قسمت‌ها را در یک UserControl قرار داد و در زمان مناسب آن را بارگذاری کرد. 
سوالی که مطرح است بارگذاری UserControl‌ها به کمک الگوی MVVM چگونه است ؟ 
کدهای XAML صفحه‌ی اصلی : 
<Window x:Class="TwoViews.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MVVM Light View Switching"
        d:DesignHeight="300"
        d:DesignWidth="300"
        DataContext="{Binding Main,
                              Source={StaticResource Locator}}"
        ResizeMode="NoResize"
        SizeToContent="WidthAndHeight"
        mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <ContentControl Content="{Binding CurrentViewModel}" />

        <DockPanel Grid.Row="1" Margin="5">
            <Button Width="75"
                    Height="23"
                    Command="{Binding SecondViewCommand}"
                    Content="Second View"
                    DockPanel.Dock="Right" />
            <Button Width="75"
                    Height="23"
                    Command="{Binding FirstViewCommand}"
                    Content="First View"
                    DockPanel.Dock="Left" />
        </DockPanel>
    </Grid>
</Window>

2 دکمه در صفحه‌ی اصلی وجود دارد ، یکی از آنها وظیفه‌ی بارگذاری View اول و دیگری وظیفه‌ی بارگذاری View دوم را دارد ،  این دکمه‌ها نقش منو را در یک نرم افزار واقعی به عهده دارند. 
کدهای View-Model گره خورده (به کمک الگوی ViewModolLocator ) به View اصلی  : 
    /// This is our MainViewModel that is tied to the MainWindow via the 
    /// ViewModelLocator class.
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        /// <summary>
        /// Static instance of one of the ViewModels.
        /// </summary>
        private static readonly SecondViewModel SecondViewModel = new SecondViewModel();
        /// <summary>
        /// Static instance of one of the ViewModels.
        /// </summary>
        private static readonly FirstViewModel FirstViewModel = new FirstViewModel();
        /// <summary>
        /// The current view.
        /// </summary>
        private ViewModelBase _currentViewModel;
        /// <summary>
        /// Default constructor.  We set the initial view-model to 'FirstViewModel'.
        /// We also associate the commands with their execution actions.
        /// </summary>
        public MainViewModel()
        {
            CurrentViewModel = FirstViewModel;
            FirstViewCommand = new RelayCommand(ExecuteFirstViewCommand);
            SecondViewCommand = new RelayCommand(ExecuteSecondViewCommand);
        }
        /// <summary>
        /// The CurrentView property.  The setter is private since only this 
        /// class can change the view via a command.  If the View is changed,
        /// we need to raise a property changed event (via INPC).
        /// </summary>
        public ViewModelBase CurrentViewModel
        {
            get { return _currentViewModel; }
            set
            {
                if (_currentViewModel == value)
                    return;
                _currentViewModel = value;
                RaisePropertyChanged("CurrentViewModel");
            }
        }
        /// <summary>
        /// Simple property to hold the 'FirstViewCommand' - when executed
        /// it will change the current view to the 'FirstView'
        /// </summary>
        public ICommand FirstViewCommand { get; private set; }
        /// <summary>
        /// Simple property to hold the 'SecondViewCommand' - when executed
        /// it will change the current view to the 'SecondView'
        /// </summary>
        public ICommand SecondViewCommand { get; private set; }
        /// <summary>
        /// Set the CurrentViewModel to 'FirstViewModel'
        /// </summary>
        private void ExecuteFirstViewCommand()
        {
            CurrentViewModel = FirstViewModel;
        }
        /// <summary>
        /// Set the CurrentViewModel to 'SecondViewModel'
        /// </summary>
        private void ExecuteSecondViewCommand()
        {
            CurrentViewModel = SecondViewModel;
        }
    }

این ViewModel از کلاس پایه‌ی چارچوب MVVM Light مشتق شده است.  Command‌ها جهت Handle کردن کلیک دکمه‌ها هستند . نکته‌ی اصلی این ViewModel پراپرتی CurrentViewModel می‌باشد.  این پراپرتی به ویژگی Content کنترل ContentControl مقید (Bind) شده است. با کلیک شدن روی دکمه‌ها View مورد نظر به کاربر نمایش داده می‌شود.
WPF از کجا می‌داند کدام View را به ازای ViewModel خاص render کند ؟ 
در فایل App.xaml یک سری DataTemplate تعریف شده است : 
    <Application.Resources>
        <vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
        <!--
            We define the data templates here so we can apply them across the
            entire application.
         
            The data template just says that if our data type is of a particular
            view-model type, then render the appropriate view.  The framework
            takes care of this dynamically.  Note that the DataContext for
            the underlying view is already set at this point, so the
            view (UserControl), doesn't need to have it's DataContext set
            directly.
        -->
        <DataTemplate DataType="{x:Type vm:SecondViewModel}">
            <views:SecondView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:FirstViewModel}">
            <views:FirstView />
        </DataTemplate>
    </Application.Resources>

به کمک این DataTemplate‌ها مشخص شده اگر نوع داده‌ی ما از یک نوع View-Model خاص می‌باشد View مناسب را به ازای آن Render کند. با تعریف DataTemplate‌ها در App.Xaml می‌توان از آنها در سطح نرم افزار استفاده کرد. می‌توان DataTemplate‌ها را جهت خلوت کردن App.xaml به Resource دیگری انتقال داد.
دریافت مثال : TwoViews.zip 

 
مطالب
فرم‌های مبتنی بر قالب‌ها در Angular - قسمت سوم - Data binding
در قسمت قبل، ساختار فرم ثبت اطلاعات کارمندان را تکمیل کردیم. در این قسمت قصد داریم این اطلاعات را در کامپوننت آن توسط data binding دریافت کنیم.


نقش ngModel در data binding

ngModel دایرکتیوی است که وجود آن سبب می‌شود تا Angular آن المان ورودی خاص را تحت نظر قرار دهد:
<!--no binding -->
<input name="firstname" ngModel>
در حالت تعریفی فوق، هیچگونه عملیات data binding ایی صورت نمی‌گیرد؛ اما Angular به علت وجود ngModel، از وجود این فیلد مطلع شده‌است. اما کامپوننت برنامه اطلاعات خاصی را دریافت نخواهد کرد.
برای رفع این مشکل می‌توان با data binding یک طرفه شروع کرد:
<!--one way binding -->
<input name="firstname" [ngModel]="firstName">
در اینجا از syntax ویژه‌ی property binding استفاده شده و ngModel داخل [] قرار گرفته‌است و به firstName تنظیم شده‌است. در این حالت Angular در کامپوننت متناظر با این قالب HTML ایی، به دنبال یک خاصیت عمومی به نام firstName می‌گردد و مقدار اولیه‌ی این فیلد را از آن دریافت می‌کند.
در حالت data binding یک طرفه، اگر کاربر اطلاعات فیلد firstname را در فرم برنامه تغییر دهد، این اطلاعات به خاصیت عمومی firstName منعکس نخواهد شد.
برای رفع این مشکل (در صورت نیاز)، می‌توان از data binding دو طرفه استفاده کرد:
<!--two way binding -->
<input name="firstname" [ngModel]="firstName"
(ngModelChange)="firstName=$event">
این حالت شبیه به حالت data binding یک طرفه است؛ با این تفاوت که رویدادگردانی ngModelChange نیز به آن اضافه شده‌است. در اینجا event$ به مقدار فیلد تغییر یافته اشاره می‌کند و آن‌را به firstName انتساب می‌دهد.
البته این حالت دو طرفه، syntax ساده شده‌ی زیر را که به banana in the box نیز معروف شده‌است (موز همان () است و جعبه به [] اشاره می‌کند)، نیز می‌تواند داشته باشد که بیشتر مورد استفاده قرار می‌گیرد:
<!--two way binding -->
<input name="firstname" [(ngModel)]="firstName">


تعریف مدل فرم ثبت اطلاعات کارمندان

برای نگهداری اطلاعات فرم کارمندان، کلاس Employee را به ماژول Employee اضافه می‌کنیم:
 > ng g cl Employee/Employee
با این خروجی:
 installing class
  create src\app\Employee\employee.ts
سپس ساختار این کلاس را به نحو ذیل تکمیل خواهیم کرد که هر کدام از خواص آن، معادل یکی از المان‌های فرم است:
export class Employee {
  constructor(
    public firstName: string,
    public lastName: string,
    public isFullTime: boolean,
    public paymentType: string,
    public primaryLanguage: string
  ) {}
}
TypeScript این امکان را می‌دهد تا بتوان خواص عمومی را مستقیما در سازنده‌ی کلاس تعریف کرد. بنابراین در اینجا برای نمونه firstName هم یکی از آرگومان‌های سازنده‌ی کلاس کارمند است و هم یک خاصیت عمومی تعریف شده‌ی در آن. به علاوه در اینجا می‌توان به این خواص، مقادیر پیش فرضی را نیز انتساب داد تا در حین وهله سازی آن بتوان از تعریف اجباری یک سری از پارامترها صرفنظر کرد.

پس از آن، به فایل employee-register.component.ts مراجعه کرده و وهله‌ای از کلاس را به صورت یک خاصیت عمومی در اختیار قالب HTML ایی آن که فرم جاری را تشکیل می‌دهد، قرار می‌دهیم:
import { Employee } from "app/employee/employee";

export class EmployeeRegisterComponent implements OnInit {
  languages = ["Persian", "English", "Spanish", "Other"];
  model = new Employee("Vahid", "N", true, "FullTime", "Persian");
ابتدا کلاس کارمند import شده و سپس وهله‌ای از آن به نام model، به صورت یک خاصیت عمومی در اختیار قالب آن قرار گرفته‌است.


تغییر قالب فرم ثبت اطلاعات کارمندان برای اتصال به model

در ادامه، مرحله به مرحله قالب فرم جاری را جهت اتصال به شیء model فوق تغییر خواهیم داد:

اتصال به Text boxes

  <form #form="ngForm" novalidate>
    <div class="form-group">
      <label>First Name</label>
      <input type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
    </div>

    <div class="form-group">
      <label>Last Name</label>
      <input type="text" class="form-control" name="lastName" [(ngModel)]="model.lastName">
    </div>
همانطور که مشاهده می‌کنید، اینبار ngModel خالی قسمت قبل را توسط syntax تکمیلی banana in the box به data binding دو طرفه تغییر داده‌ایم. به این ترتیب در ابتدای نمایش فرم، این دو فیلد، مقادیر اولیه نام و نام خانوادگی را از شیء model دریافت کرده و نمایش می‌دهند. به علاوه اگر فرم نیز تغییر کند، این اطلاعات به شیء model و خواص آن نیز منعکس می‌شوند.

برای بررسی این مورد، در پایان فرم جهت دیباگ data binding، اطلاعاتی را که در مدل داریم و همچنین اطلاعاتی را که Angular در حال نظارت بر آن‌ها است، به صورت json در صفحه درج می‌کنیم:
    <button class="btn btn-primary" type="submit">Ok</button>
  </form>
  Model: {{ model | json }}
  <br> Angular: {{ form.value | json }}
  <br> form.pristine: {{ form.pristine }}
برای مثال یکبار [()] را به [] تبدیل کنید و سپس سعی در تغییر مقادیر فرم نمائید. مشاهده می‌کنید هرچند این اطلاعات تحت نظارت Angular هستند، اما چون data binding به حالت یک طرفه تغییر کرده‌است، دیگر انعکاس آن‌ها، در Model مشاهده نمی‌شوند.


اتصال به Check boxes

    <div class="checkbox">
      <label>
            <input type="checkbox" name="is-full-time"
                   [(ngModel)]="model.isFullTime"> Full Time Employee
            </label>
    </div>
روش کار در اینجا نیز همانند قبل است. با استفاده از data binding دو طرفه، مقدار checkbox را به یک خاصیت عمومی boolean انتساب داده‌ایم و برعکس (زمانیکه فرم برای بار اول نمایش داده می‌شود، مقدار اولیه‌ی خود را از شیء model دریافت می‌کند).

اتصال به Radio buttons

    <label>Payment Type</label>
    <div class="radio">
      <label>
            <input type="radio" name="paymentType" value="FullTime" checked
                   [(ngModel)]="model.paymentType">
                Full Time
            </label>
    </div>
    <div class="radio">
      <label>
            <input type="radio" name="paymentType" value="PartTime"
                   [(ngModel)]="model.paymentType">
                Part Time
            </label>
    </div>
روش اتصال به radio buttons نیز بر اساس data binding دو طرفه‌است. فقط در اینجا دقیقا یک خاصیت مشخص، به چندین radio button متصل شده‌است و در نهایت در این گروه که بر اساس name هایی یکسان تشکیل شده‌است، یک مقدار انتخاب می‌شود و مقدار آن از ویژگی value المان متناظر دریافت می‌گردد.

اتصال به Drop downs

    <div class="form-group">
      <label>Primary Language</label>
      <select class="form-control" name="primaryLanguage" [(ngModel)]="model.primaryLanguage">
          <option *ngFor="let lang of languages">
             {{ lang }}
          </option>
      </select>
    </div>
در اینجا نیز ابتدا نامی به این المان انتساب داده شده‌است و سپس توسط data binding دو طرفه، خاصیت متناظری از مدل را به این المان متصل کرده‌ایم یا برعکس؛ زمانیکه این فرم برای اولین بار نمایش داده می‌شود، مقدار اولیه‌ی این فیلد بر اساس مقدار آن در شیء model تعیین می‌شود:



نحوه‌ی فراخوانی یک متد در حین data binding دو طرفه

همانطور که در ابتدای بحث نیز عنوان شد، data binding دو طرفه را به نحو دیگری نیز می‌توان تعریف کرد:

<div class="form-group">
            <label>First Name</label>
            <input type="text" class="form-control" name="firstName" 
                [ngModel]="model.firstName" 
                (ngModelChange)="firstNameToUpperCase($event)">
</div>
در اینجا بجای استفاده‌ی از syntax معروف banana in the box، از روش اتصال یک طرفه و سپس دریافت تغییرات از طریق یک رخدادگردان استفاده شده‌است. مزیت این روش امکان دسترسی همزمان به مقدار وارد شده‌ی توسط کاربر، در کامپوننت متناظر می‌باشد:
  firstNameToUpperCase(value: string) {
    if (value.length > 0)
      this.model.firstName = value.charAt(0).toUpperCase() + value.slice(1);
    else
      this.model.firstName = value;
  }
برای مثال در اینجا اگر کاربر حرف اول یک نام را با حروف کوچک وارد کند، توسط این متد به حرف بزرگ تبدیل شده و جایگزین می‌شود. این جایگزینی نیز بلافاصله در فرم منعکس خواهد شد.

در قسمت بعد مباحث اعتبارسنجی فرم‌های مبتنی بر قالب‌ها را بررسی می‌کنیم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-03.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
نظرات مطالب
آشنایی با الگوی MVP
تفاوت مهم این‌ها در قسمت ViewModel و Presenter هست که در MVVM قسمت ViewModel آن پیاده سازی کننده‌ی الگوی observer است و تغییرات اطلاعات در مدل یا حتی خواص قرار گرفته‌ی در آن، بلافاصله به View منعکس می‌شوند و برعکس آن هم با two-way binding میسر است.
در MVP، قسمت Presenter از وجود View مطلع هست. برای نمونه در مثال بحث فوق، شیء this را دریافت می‌کند و به این ترتیب هست که قابلیت به روز رسانی آن‌را خواهد یافت. اما در MVVM قسمت ViewModel، هیچ ارجاعی را از View درخود ندارد.

بر این مبنا MVVM برای برنامه‌های statefull مانند برنامه‌های دسکتاپ و یا حتی برنامه‌های تک صفحه‌ای وب سمت کاربر (استفاده از knockout js) مرسوم است.
MVP برای برنامه‌های stateless مانند برنامه‌های سمت سرور وب بیشتر کاربرد دارد. جایی که یک فریم ورک binding حاصل از زنده نگه داشتن context برنامه در دسترس نیست و طول عمر هر صفحه محدود است به پایان Response آن.
مطالب
آموزش Knockout.Js #2
در پست قبلی با مفاهیم و ویژگی‌های کلی KO آشنا شدید. KO از الگوی طراحی MVVM استفاده می‌کند. از آن جا که یکی از پیش نیاز‌های KO آشنایی اولیه با مفاهیم View و Model است نیاز به توضیح در این موارد نیست اما اگر به هر دلیلی با این مفاهیم آشنایی ندارید می‌توانید از اینجا شروع کنید. اما درباره ViewModel که کمی مفهوم متفاوتی دارد، این نکته قابل ذکر است که KO از ViewModel برای ارتباط مستقیم بین View و Model استفاده می‌کند، چیزی شبیه به منطق MVC با این تفاوت که ViewModel به جای Controller قرار خواهد گرفت.

ابتدا باید به شرح برخی مفاهیم در KO بپردازم:
»Observable(قابل مشاهده کردن تغییرات)
KO از Observable برای ردیابی و مشاهده تغییرات خواص ViewModel استفاده می‌کند. در واقع Observable دقیقا شبیه به متغیر‌ها در JavaScript عمل می‌کنند با این تفاوت که به KO اجازه می‌دهند که تغییرات این خواص را پیگیری کند و این تغییرات را به بخش‌های مرتبط View اعمال نماید. اما سوال این است که KO چگونه متوجه می‌شود که این تغییرات بر کدام قسمت در View تاثیر خواهند داشت؟ جواب این سوال در مفهوم Binding است.
»Binding
برای اتصال بخش‌های مختلف View به Observable‌ها باید از binding(مقید سازی) استفاده کنیم. بدون عملیات binding، امکان اعمال تغییرات Observable‌ها بر روی عناصر HTML امکان پذیر نیست.
برای مثال در شکل زیر یکی از خواص ViewModel را به View متناظر مقید شده است.

با کمی دقت در شکل بالا این نکته به دست می‌آید که می‌توان در یک ViewModel، فقط خواص مورد نظر را به عناصر Html مقید کرد.

دانلود فایل‌های مورد نیاز

فایل‌های مورد نیاز برای KO رو می‌توانید از اینجا دانلود نمایید و به پروژه اضافه کنید. به صورت پیش فرض فایل‌های مورد نیاز KO، در پروژه‌های MVC 4 وجود دارد و نیاز به دانلود آن‌ها نیست و شما باید فقط مراحل BundleConfig را انجام دهید.

تعریف ViewModel

برای تعریف ViewModel و پیاده سازی مراحل Observable و binding باید به صورت زیر عمل نمایید:

<html lang='en'>
<head>
<title>Hello, Knockout.js</title>
<meta charset='utf-8' />
<link rel='stylesheet' href='style.css' />
</head>
<body>
<h1>Hello, Knockout.js</h1>
<script type='text/javascript' src='knockout-2.1.0.js'>   
      <script type='text/javascript'>
             var personViewModel = {
                  firstName: "Masoud",
                  lastName: "Pakdel"
                };
                 ko.applyBindings(personViewModel);
       </script>
</script>
</body>
</html>
مشاهده می‌کنید که ابتدا یک ViewModel به نام person ایجاد کردم همراه با دو خاصیت به نام‌های firstName و lastName. تابع applyBinding برای KO بدین معنی است که این آبجکت به عنوان یک ViewModel در این صفحه مورد استفاده قرار خواهد گرفت. اما برای مشاهده تغییرات باید یک عنصر HTML را یه این ViewModel مقید(bind) کنیم.

مقید سازی عناصر HTML

برای مقید سازی عناصر HTML به ViewModel‌ها باید از data-bind attribute استفاده نماییم. برای مثال:
<p><span data-bind='text: firstName'></span>'s Shopping Cart</p>
اگر به data-bind در تگ span بالا توجه کنید خواهید دید که مقدار text در این تگ را به خاصیت firstName در viewModel این صفحه bind شده است. تا اینجا KO می‌داند که چه عنصر از DOM به کدام خاصیت از ViewModel مقید شده است اما هنوز دستور ردیابی تغییرات(Observable) را برای KO تعیین نکردیم.

چگونه خواص را Observable کنیم
در پروژه‌های WPF، فقط در صورتی تغییرات خواص یک کلاس ردیابی می‌شوند که اولا کلاس اینترفیس INotifyPropertyChanged را پیاده سازی کرده باشد ثانیا، در متد set این خواص، متد OnPropertyChanged(البته این متد می‌تواند هر نام دیگری نیز داشته باشد) صدا زده شده باشد. نکته مهم و اساسی در KO نیز همین است که برای اینکه KO بتواند تغییرات هر خاصیت را مشاهده کند حتما خواص مورد نظر  باید Observable  شوند. برای این کار کافیست به صورت عمل کنید:
var personViewModel = {
  firstName: ko.observable("Masoud"),
  lastName: ko.observable("Pakdel")
};
مزیت اصلی برای اینکه حتما خواص مورد نظرتان  Observable شوند این است که، در صورتی که مایل نباشید تغییرات یک خاصیت  بر روی View اعمال شود کافیست از دستور بالا استفاده نکنید. درست مثل اینکه هرگز مقدار آن تغییر نکرده است.

پیاده سازی متد‌های get و set
همان طور که متوجه شدید، Observable‌ها متغیر نیستند بلکه تابع هستند در نتیجه برای دستیابی به مقدار یک observable کافیست آن را بدون پارامتر ورودی صدا بزنیم و برای تغییر در مقدار آن باید همان تابع را با مقدار جدید صدا بزنیم. برای مثال:
personViewModel.firstName() // Get
personViewModel.firstName("Masoud") // Set
البته این نکته را هم متذکر شوم که در ViewModel‌های خود می‌توانید توابع سفارشی مورد نیاز را بنویسید و از آن‌ها در جای مناسب استفاده نماید(شبیه به مفاهیم Command‌ها در WPF)
مقید سازی تعاملی
اگر با WPF آشنایی دارید می‌دانید که در این گونه پروژه‌ها می‌توان رویداد‌های مورد نظر را به Command‌های خاص در ViewModel مقید کرد. در KO نیز این امر به آسانی امکان پذیر است که به آن Interactive Bindings می‌گویند. فقط کافیست در data-bind attribute  از نام رویداد استفاده نماییم. مثال:
ایتدا بک ViewModel به صورت زیر خواهیم داشت:
function PersonViewModel() {
   this.firstName = ko.observable("Masoud");
   this.lastName = ko.observable("Pakdel");
   this.clickMe= function() {
    alert("this is test!");
  };
};
تنها نکته قابل ذکر تعریف تابع سفارشی به نام clickMe است که به نوعی معادل Command مورد نظر ما در WPF است.  در عنصر HTML مورد نظر که در این جا button است باید data-binding به صورت زیر باشد:
<button data-bind='click: clickMe'>Click Me...</button>
در نتیجه بعد از کلیک بر روی button بالا تابع مورد نظر در viewModel اجرا خواهد شد.
پس به صورت خلاصه:
  • ابتدا ViewModel مورد نظر را ایجاد نمایید؛
  • سپس با استفاده از data-bind عملیات مقید سازی بین View و ViewModel را انجام دهید
  • در نهایت با استفاده از Obsevable تغییرات خواص مورد نظر را ردیابی نمایید.

ادامه دارد...