۱۳۹۱ بهمن ۱۴, شنبه

نوشتن یک Module در Prism

قبل از شروع خواندن این پست ممکن است لازم باشد با Bootstrapper در پست قبلی، همچنین الگوی طراحی MVVM آشنا باشید.

قبل از هر چیز باید Shell برنامه را برای پذیرش View های جدید از Module ها، آماده کنیم. همینطور ممکن است بخواهیم در Shell برنامه یک Ribbon داشته باشیم تا Module ها بتوانند به آن RibbonTab های خود را اضافه کنند. پس از اطمینان از دانلود و نصب آخرین نسخه Ribbon Control Library برای دات نت 4، روی پروژه PrismApp.Shell راست کلیک کنید و پس از انتخاب گزینه Add Reference، در زبانه Extensions آیتم RibbonControlsLibrary را انتخاب کنید. اگر از Visual Studio 2010 استفاده می کنید احتمالا بتوانید RibbonControlsLibrary را در این مسیر پیدا کنید:

C:\Program Files (x86)\Microsoft Ribbon for WPF\V4.0

بسیار خوب. اکنون باید کد MainWindow.xaml را به این شکل تغییر دهید:

<ribbon:RibbonWindow x:Class="PrismApp.Shell.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://www.codeplex.com/prism"
        xmlns:ribbon="http://schemas.microsoft.com/winfx/2006/xaml/presentation/ribbon"
        Title="Prism App" Height="550" Width="725" Style="{StaticResource WindowStyle}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="{Binding Height, ElementName=MainRibbon}" ></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <ribbon:Ribbon x:Name="MainRibbon" Grid.Row="0" prism:RegionManager.RegionName="RibbonRegion">
            <ribbon:Ribbon.ApplicationMenu>
                <ribbon:RibbonApplicationMenu Visibility="Collapsed">
                </ribbon:RibbonApplicationMenu>
            </ribbon:Ribbon.ApplicationMenu>
        </ribbon:Ribbon>
        <TabControl prism:RegionManager.RegionName="MainRegion" Margin="5" Grid.Row="2" />
    </Grid>
</ribbon:RibbonWindow>

پنجره اصلی برنامه از RibbonWindow ساخته شده است. فراموش نکنید در CodeBehind (فایل MainWindow.xaml.cs) نیز کلاس پدر را از Window به RibbonWindow تغییر بدهید. در قسمت ارجاعات ما دو ارجاع به ribbon و prism داریم تا بتوانیم از کلاس ها و extension های آن ها در این پنجره استفاده کنیم. پس از افزودن یک Ribbon، یک TabControl نیز در داخل Grid اصلی قرار دادیم. بدین صورت هر module می تواند View های خود را تحت یک TabItem نمایش دهد. ولی چطور؟
به کمک Region ها شما می توانید برخی کنترل ها را به عنوان میزبانی برای View های دیگر معرفی کنید. بدین صورت بعدا می توانید به Region ها دسترسی پیدا کنید و View های داخل آن ها را مطابق نیاز خود عوض کنید.
هر Region باید دارای یک نام منحصر به فرد باشد. ما در این پروژه، Region ی که قرار است TabItem ها را در خود نگه دارد را MainRegion نامیدیم. (به کد TabControl دقت کنید.)

<TabControl prism:RegionManager.RegionName="MainRegion" Margin="5" Grid.Row="2" />

همینطور در کد Ribbon، یک Region به نام RibbonRegion ایجاد کردیم تا module ها بتوانند RibbonTab های خود را به آن اضافه کنند. در همین پروژه یک پوشه به نام Themes بسازید و درون آن یک ResourceDictionary به نام Generic.xaml قرار دهید. سپس Style های زیر را در آن بنویسید:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:ribbon="http://schemas.microsoft.com/winfx/2006/xaml/presentation/ribbon">
    <Style TargetType="Window" x:Key="WindowStyle">
        <Setter Property="FontFamily" Value="Tahoma" />
        <Setter Property="FontSize" Value="12" />
    </Style>
    
    <Style TargetType="ribbon:Ribbon">
        <Setter Property="FontFamily" Value="Tahoma" />
        <Setter Property="FontSize" Value="12" />
    </Style>

    <Style TargetType="TabControl">
        <Setter Property="HorizontalAlignment" Value="Stretch" />
        <Setter Property="VerticalAlignment" Value="Stretch" />
        <Setter Property="HorizontalContentAlignment" Value="Stretch" />
        <Setter Property="VerticalContentAlignment" Value="Stretch" />
    </Style>

    <Style TargetType="TabItem">
        <Setter Property="HorizontalAlignment" Value="Stretch" />
        <Setter Property="VerticalAlignment" Value="Stretch" />
        <Setter Property="HorizontalContentAlignment" Value="Stretch" />
        <Setter Property="VerticalContentAlignment" Value="Stretch" />
    </Style>
</ResourceDictionary>

سپس App.xaml را به این شکل تغییر دهید:

<Application x:Class="PrismApp.Shell.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Themes/Generic.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

با این کار، Style های تعریف شده در کل برنامه در دسترس خواهند بود. اگر همه چیز مرتب باشد باید بتوانید پروژه را کامپایل کرده و فرم خالی برنامه را ببینید. در صورتی که با خطای زیر مواجه شدید، پوشه Debug پروژه را پاک کنید و مجددا برنامه را اجرا کنید:
An unhandled exception of type 'System.StackOverflowException' occurred in System.Core.dll
حالا باید سراغ module برویم.
روی solution راست کلیک کرده و پروژه جدیدی از نوع WPF User Control با نام PrismApp.Modules.MyFirstModule ایجاد کنید. فراموش نکنید module ما باید به کتابخانه های RibbonControlLibrary ،Prism ،Unity و Prism.UnityExtensions ارجاع داشته باشد. پس reference های لازم را به این شکل در Package Manager Console اضافه کنید:

PM> install-package Prism -ProjectName PrismApp.Modules.MyFirstModule
PM> install-package Unity -ProjectName PrismApp.Modules.MyFirstModule
PM> install-package Prism.UnityExtensions -ProjectName PrismApp.Modules.MyFirstModule

نگران دانلود مجدد آن ها نباشید. nuget ابتدا برسی می کند که آیا قبلا این package ها را دانلود کرده است یا نه، سپس اقدام به نصب آن ها می کند. RibbonControlLibrary از طریق nuget در دسترس نیست. reference آن را بطور دستی به پروژه  ی module اضافه کنید. همچنین فایل پیش فرض xaml داخل پروژه module را پاک کنید. دو پوشه به نام های Views و ViewModels بسازید. سپس در پوشه Views دو WPF User Control با نامهای MyModuleMainView.xaml و MyModuleRibbonTab.xaml بسازید. 
فایل MyModuleMainView.xaml را به این شکل تغییر دهید:

<TabItem x:Class="PrismApp.Modules.MyFirstModule.Views.MyModuleMainView"
             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" Header="My First Module"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
            <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding Message}"></TextBlock>            
    </Grid>
</TabItem>

از آنجایی که در Shell اصلی برنامه، یک TabControl میزبان View های ماست، پس بهتر است View های ماژول ما TabItem باشند. فراموش نکنید در فایل MyModuleMainView.xaml.cs نیز کلاس پدر را از UserControl به TabItem تغییر دهید. حالا همین کار را با MyModuleRibbonTab.xaml و فایل Code Behind مربوطه انجام می دهیم، با این تفاوت که این بار آن را به کلاس RibbonTab تبدیل می کنیم. فایل نهایی باید چیزی شبیه این باشد:

<ribbon:RibbonTab x:Class="PrismApp.Modules.MyFirstModule.Views.MyModuleRibbonTab"
             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:ribbon="http://schemas.microsoft.com/winfx/2006/xaml/presentation/ribbon"
xmlns:commands="clr-namespace:PrismApp.Modules.MyFirstModule.Commands"
             Header="My Module"
             mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300">
    <ribbon:RibbonGroup Header="My Module Items">
        <ribbon:RibbonButton Content="Save" Command="{x:Static commands:ModuleCommands.SaveCommand}"></ribbon:RibbonButton>
    </ribbon:RibbonGroup>
</ribbon:RibbonTab>
در این فایل یک ارجاع به فضای نامی ViewModels وجود دارد، همچنین در RibbonButton، مشخصه Command به یک مشخصه static در کلاس ModuleCommands متصل شده (bind) است. در ادامه این کلاس را ایجاد می کنیم:
پوشه ای به نام Commands بسازید و کلاس ModuleCommands را در آن ایجاد کنید:

using Microsoft.Practices.Prism.Commands;

namespace PrismApp.Modules.MyFirstModule.Commands
{
    public static class ModuleCommands
    {
        public static readonly CompositeCommand SaveCommand = new CompositeCommand();
    }
}

در این کلاس، فیلد عمومی به نام SaveCommand ایجاد کرده ایم تا از تمامی قسمت های ماژول به آن دسترسی داشته باشیم. نوع این فیلد CompositeCommand است که اجازه می دهد کلاس های ViewModel (یا هر کلاس دیگری) دیگر فرمان های مربوط به عمل Save خود را در آن ثبت کنند (Register) و با هر بار فراخوانی SaveCommand، تمامی Command های ثبت شده در آن اجرا (Execute) می شوند.
در پوشه ViewModels یک کلاس به نام MyModuleMainViewModel بسازید:

using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.ViewModel;

namespace PrismApp.Modules.MyFirstModule.ViewModels
{
    public class MyModuleMainViewModel : NotificationObject
    {
        private readonly INameService _textService;
        public MyModuleMainViewModel(INameService textService)
        {
                   _textService = textService;
            this.SaveCommand = new DelegateCommand(Save, CanSave);
             ModuleCommands.SaveCommand.RegisterCommand(SaveCommand);
        }


        private string _message = "Hello from My Module!";

        public string Message
        {
            get { return _message; }
            set
            {
                if (_message != value)
                {
                    _message = value;
                    RaisePropertyChanged(() => Message);
                }
            }
        }

        public DelegateCommand SaveCommand { get; private set; }

        public void Save()
        {
            this.Message =  string.Format("Save Command Executed for {0}.", _textService.GetRandomName());
        }

        public bool CanSave()
        {
            return true;
        }
    }
}

اگر با الگوی MVVM آشنا باشید می دانید که در ViewModel ها باید واسط INotifyPropertyChanged پیاده سازی شود. در Prism ما کلاس NotificationObject را پیاده سازی می کنیم. این کلاس علاوه بر پیاده سازی واسط مذکور، تابع کمکی برای فراخوانی رویداد PropertyChanged را نیز دارد. تابع RaisePropertyChanged پارامتری به شکل عبارت Lambda می پذیرد (علاوه بر حالت رشته ای) که احتمال اشتباه را در نوشتن نام Property ناممکن می کند. همچنین در صورت تغییر نام Property در تعریف، این ارجاع بطور اتوماتیک تغییر می کند. 
در این ViewModel ما یک Command از نوع کلاس DelegateCommand ایجاد کردیم. این کلاس Prism، یک پیاده سازی از واسط ICommand است. هنگام نمونه سازی از آن در تابع سازنده، دو تابع Save و CanSave را به آن ارجاع دادیم. سپس آن را در ModuleCommands.SaveCommand ثبت کردیم تا هنگامی که کاربر بر روی RibbonButton کلیک می کند، این Command اجرا شود. هنگام اجرای دستور، متنی به کاربر نمایش داده می شود که قسمتی از آن توسط سرویس INameService فراهم می شود. هدفم از قرار دادن این سرویس، آشنایی با نحوه پیاده سازی و استفاده از سرویس ها در برنامه های MVVM است.
سرویس INameService بسیار ساده است. پوشه ای به نام Services ایجاد کنید و یک واسط به نام INameService و یک کلاس به نام NameService در آن ایجاد کنید:

// INameService.cs file
namespace PrismApp.Modules.MyFirstModule.Services
{
    public interface INameService
    {
        string GetRandomName();
    }
}

// NameService.cs file
namespace PrismApp.Modules.MyFirstModule.Services
{
    using System;
    public class NameService : INameService
    {
        private readonly string[] _texts = { "Jalal", "Majid", "Saeed", "Alireza", "Esmaeil" };
        private readonly Random _random = new Random();

        public string GetRandomName()
        {
            // return random name...
            return _texts[_random.Next(0, _texts.Length)];
        }
    }
}


واسط INameService فقط اعلام می کند که قابلیت ارائه یک نام تصادفی را می پذیرد، کلاس NamedService نیز این قابلیت را برای آن پیاده سازی می کند.
در کل، هدف از ایجاد واسط ها و سپس کلاس های پیاده سازی کننده آن ها، جدا کردن جریان کار از نحوه پیاده سازی آن است. در اینجا، ViewModel ما فقط یک نام تصادفی می خواهد، و نیازی نیست بداند این نام تصادفی از کجا (آرایه؟ وب سرویس؟ بانک اطلاعاتی؟ ...) آمده است. بدین صورت شما ممکن است چندین پیاده سازی متفاوت از INameService داشته باشید و وابسته به شرایط و یا تنظیمات نرم افزار، از یکی از آنها را انتخاب کنید.
اکنون نوبت پیاده سازی کلاس MyFirstModule است. این کلاس نقطه اجرای ماژول ما است:


using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.Unity;
using PrismApp.Modules.MyFirstModule.Services;
using PrismApp.Modules.MyFirstModule.ViewModels;
using PrismApp.Modules.MyFirstModule.Views;
using System;

namespace PrismApp.Modules.MyFirstModule
{
    public class MyFirstModule : IModule
    {
        private readonly IUnityContainer _container;
        private readonly IRegionManager _regionManager;

        public MyFirstModule(IUnityContainer container, IRegionManager regionManager)
        {
            _container = container;
            _regionManager = regionManager;
        }

        public void Initialize()
        {
            // Register Services
            _container.RegisterType<INameService, NameService>();

            // Register ViewModels
            _container.RegisterType<MyModuleMainViewModel>();

            //Register Views
            _container.RegisterType<Object, MyModuleMainView>();
            _container.RegisterType<Object, MyModuleRibbonTab>();

            // Register views inside named regions
            _regionManager.RegisterViewWithRegion("RibbonRegion", ResolveRibbonTabView);
            _regionManager.RegisterViewWithRegion("MainRegion", ResolveMainView);
        }

        private object ResolveRibbonTabView()
        {
            return _container.Resolve<MyModuleRibbonTab>();
        }

        private object ResolveMainView()
        {
            return _container.Resolve<MyModuleMainView>();
        }
    }
}

اگر به خاطر داشته باشید، در Bootstrapper، تابعی به نام CreateModuleCatalog وجود داشت که وظیفه آن بارگذاری ماژول ها بود. به تابع سازنده این کلاس دقت کنید. دو پارامتر IUnityContainer و IRegionManager، هر دو توسط Unity در کلاس Bootstrapper مقدار دهی شده و هنگام بارگذاری ماژول، به تابع سازنده آن تزریق می شوند.
در ادامه تابع Initialize توسط Bootstrapper فراخوانی می شود که شروع به ثبت سرویس ها، View و ViewModel ها در Unity می کند. در ادامه، View های مربوطه را در Region های از پیش مشخص شده (نام گذاری شده) ثبت می کند. بدین صورت (در این مثال) هنگام اجرای ماژول، View ها ایجاد شده و بر اساس نام مشخص شده، در Region مربوط به خود قرار می گیرند.
در صورتی که همه چیز مرتب باشد باید بتوانید ماژول خود را compile کنید. سپس به پوشه Bin\Debug آن رفته و assembly ماژول را (که PrismApp.Modules.MyFirstModule.dll نام دارد) در مسیر فایل اجرای برنامه (که PrismApp.Shell.exe) کپی نمایید. البته ویژوال استودیو قابلیت اتوماتیک کردن این روال را برای هر بار Build ماژول دارد. پس برای کار آسان شود، روی پروژه ماژول راست کلیک کرده و Properties را انتخاب کنید. زبانه Build Events را انتخاب کنید، سپس در کادر متنی Post-build events command line فرمان زیر را کپی کنید:
xcopy "$(TargetDir)*.*" "$(SolutionDir)PrismApp.Shell\bin\$(ConfigurationName)\" /Y
در صورتی که هنگام Compile ماژول به خطای زیر برخوردید، احتمالا این فرمان را (شاید چون نام پروژه شما متفاوت است) اشتباه نوشته اید.

سورس کد این قسمت را تحت عنوان Step3 از github دانلود کنید.

هیچ نظری موجود نیست: