۱۳۹۰/۱۰/۱۲

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>