Методология синхронной разработки приложения в Microsoft Visual Studio 2010

Построение кроссплатформенного Silverlight/WPF приложения

Разбить на страницы
Показывать лекцию целиком

Цель лабораторной работы: показать читателям на примере фрагментов кода полную реализацию многослойного кроссплатформенного Silverlight/WPF MVVM приложения. Данная лабораторная работа является обобщающим занятием по курсу.

Для более детального представления можно скачать исходный код итогового проекта

Создание проектов

В первую очередь необходимо создать проекты будущего приложения. В простейшем случае это будут 3 сборки: WPF Application, Silverlight Application, а также Web Application для хостинга Silverlight приложения.

Окно создание проекта вызывается нажатием пункта меню File – New – Project:

(рис 18.1) Вызов окна создания проекта

В появившемся окне необходимо перейти в раздел Other Project Types – Visual Studio Solutions и выбрать Blank Solution:

(рис 18.2) Создание пустого решения

В созданное решение следует добавить Silverlight и WPF проекты, используя контекстное меню обозревателя решений:

(рис 18.3) Добавление проекта в решение

Для добавления Silverlight приложения необходимо выбрать пункт Silverlight Application в разделе Visual C# – Silverlight:

(рис 18.4) Добавление Silverlight приложения

В появившемся диалоговом окне следует подтвердить создание Web Application для размещения Silverlight приложения и выбрать версию Silverlight:

(рис 18.5) Задание параметров Silverlight приложения

Аналогично необходимо добавить WPF приложение:

(рис 18.6) Добавление WPF приложения

Вместе с проектами были автоматически созданы главные окна, MainWindow.xaml и MainPage.xml, для WPF и Silverlight соответственно. Так как эти сущности различны в двух версиях (в случае WPF это окно, в Silverlight – UserControl), их невозможно повторно использовать. Однако в них можно поместить приведенный в лекции 11 элемент управления ViewModelPresenter, который будет делегировать создание внутренней разметки представлению, соответствующему находящейся в DataContext модели представления.

MainWindow.xaml:

  1: <Window x:Class="CrossPlatformApplication.MainWindow"
  2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4: xmlns:view="clr-namespace:CrossPlatformApplication"
  5: Title="{Binding Title}" SizeToContent="WidthAndHeight">
  6:     
  7:     <view:ViewModelPresenter ViewModel="{Binding}" />
  8: </Window>

MainPage.xaml:

  1: <UserControl x:Class="SilverlightApplication.MainPage"
  2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4: xmlns:view="clr-namespace:CrossPlatformApplication">
  5:
  6:     <view:ViewModelPresenter ViewModel="{Binding}" />
  7: </UserControl>

Согласно принципам MVVM главному представлению приложения должна соответствовать модель представления. Для начала достаточно пустого класса, общего для WPF и Silverlight:

  1: [Export]
  2: [PartCreationPolicy(CreationPolicy.NonShared)]
  3: [ExportMetadata(AopExtensions.AspectMetadata,
  4:     Aspects.NotifyPropertyChanged)]
  5: public class MainViewModel : IEntitledViewModel
  6: {
  7:     public string Title
  8:     {
  9:         get { return "Test Application"; }
 10:     }
 11: }

Затем для корректного запуска приложения необходимо добавить логику инициализации IoC контейнера MEF и MVVM окружения, а также модифицировать логику отображения главных окон. Как в WPF, так и в Silverlight приложении модифицируется код App.xaml и App.xaml.cs.

App.xaml в WPF:

  1: <Application x:Class="WpfApplication.App"
  2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  4:     <Application.Resources>
  5:          
  6:     </Application.Resources>
  7: </Application>

App.xaml.cs в WPF:

  1: public partial class App
  2: {
  3:     #region Injected properties
  4:
  5:     [Import]
  6:     public ChildViewManager ChildViewManager
  7:        { private get; set; }
  8:
  9:     #endregion
 10:
 11:     protected override void OnStartup(StartupEventArgs e)
 12:     {
 13:         base.OnStartup(e);
 14:
 15:         // Create interception configuration
 16:         var cfg = new InterceptionConfiguration()
 17:             .AddAopInterception();
 18:
 19:         var container = new CompositionContainer(
 20:            new InterceptingCatalog(new AggregateCatalog(
 21:            new DirectoryCatalog(".", "*.exe"),
 22:            new DirectoryCatalog(".", "*.dll")), cfg));
 23:
 24:         var locator = new MefServiceLocator(container);
 25:         ServiceLocator.SetLocatorProvider(() => locator);
 26:         container.ComposeExportedValue<IServiceLocator>(locator);
 27:         container.ComposeParts(this);
 28:
 29:         new MainWindow { DataContext =
 30:                   locator.GetInstance<MainViewModel>() }.Show();
 31:     }
 32: }

App.xaml в Silverlight:

  1: <Application x:Class="SilverlightApplication.App"
  2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  4:     <Application.Resources>
  5:         
  6:     </Application.Resources>
  7: </Application>

App.xaml.cs в Silverlight:

  1: public partial class App
  2: {
  3:     public App()
  4:     {
  5:         Startup += Application_Startup;
  6:         Exit += Application_Exit;
  7:         UnhandledException += Application_UnhandledException;
  8:
  9:         InitializeComponent();
 10:     }
 11:
 12:     #region Injected properties
 13:
 14:     [Import]
 15:     public ChildViewManager ChildViewManager
 16:        { private get; set; }
 17:
 18:     #endregion
 19:
 20:     private void Application_Startup
 21:        (object sender, StartupEventArgs e)
 22:     {
 23:         // Create interception configuration
 24:         var cfg = new InterceptionConfiguration()
 25:             .AddAopInterception();
 26:
 27:         var container = new CompositionContainer(
 28:                         new InterceptingCatalog(
 29:                         new DeploymentCatalog(), cfg));
 30:         var locator = new MefServiceLocator(container);
 31:         ServiceLocator.SetLocatorProvider(() => locator);
 32:         container.ComposeExportedValue<IServiceLocator>(locator);
 33:         container.ComposeParts(this);
 34:
 35:         RootVisual = new MainPage
 36:           { DataContext = locator.GetInstance<MainViewModel>() };
 37:     }
 38:
 39:     private void Application_Exit(object sender, EventArgs e)
 40:     {
 41:
 42:     }
 43:
 44:     private void Application_UnhandledException
 46:        (object sender, ApplicationUnhandledExceptionEventArgs e)
 47:     {
 48:         if (!System.Diagnostics.Debugger.IsAttached)
 49:         {
 50:             e.Handled = true;
 51:             Deployment.Current.Dispatcher.BeginInvoke(
 52:                delegate { ReportErrorToDOM(e); });
 53:         }
 54:     }
 55:
 56:     private void ReportErrorToDOM
 57:        (ApplicationUnhandledExceptionEventArgs e)
 44:     {
 45:         try
 46:         {
 47:             string errorMsg = e.ExceptionObject.Message
 48:                + e.ExceptionObject.StackTrace;
 49:             errorMsg = errorMsg.Replace('"',
 50:                '\'').Replace("\r\n", @"\n");
 51:
 52:             System.Windows.Browser.HtmlPage.Window.Eval(
 53:   "throw new Error(\"Unhandled Error in Silverlight Application "
 54:   + errorMsg + "\");");
 55:         }
 56:         catch (Exception)
 57:         {
 58:         }
 59:     }
 60: }

Как видно из кода, главные окна приложения будут отображать представление MainView, соответствующее модели представления MainViewModel. Пусть это представление будет содержать простой текст "Hello world":

  1: <UserControl x:Class="CrossPlatformApplication.MainView"
  2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4: xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5: xmlns:mc
  6:    ="http://schemas.openxmlformats.org/markup-compatibility/2006"
  7: mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
  8:     
  9:     <Grid x:Name="LayoutRoot" Background="White" MinHeight="150"
 10:           MinWidth="200">
 11:         <TextBlock Text="Hello world"
 12:                    HorizontalAlignment="Center"
 13:                    VerticalAlignment="Center" />
 14:     </Grid>
 15: </UserControl>

Естественно, как говорилось в лекции 12, необходимо создать представление в Silverlight проекте и добавить ссылку на него в WPF проект. Теперь оба приложения готовы к запуску и отображают на своем главном окне сообщение "Hello world".

Следующим этапом следует некоторое усложнение примера – размещенная на главном представлении кнопка будет отображать модальное дочернее окно, считывающее строку у пользователя. Введенные данные модифицируют исходный текст на главном представлении.

В первую очередь необходимо создать модель представления диалога, которая наследуется от класса ModalViewModelBase:

  1: [Export]
  2: [PartCreationPolicy(CreationPolicy.NonShared)]
  3: [ExportMetadata(AopExtensions.AspectMetadata,
  4:    Aspects.NotifyPropertyChanged)]
  5: public class TextInputModalChildViewModel :
  6:    ModalChildViewModelBase
  7: {
  8:     public virtual string Text { get; set; }
  9: }

Далее создается представление, содержащее поле ввода, привязанное к свойству Text:

  1: <UserControl
  2: x:Class="CrossPlatformApplication.TextInputModalChildView"
  3: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  5: xmlns:i=
  6:    "http://schemas.microsoft.com/expression/2010/interactivity"
  7: xmlns:ic=
  8:    "http://schemas.microsoft.com/expression/2010/interactions"
  9: xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 10: xmlns:mc=
 11:    "http://schemas.openxmlformats.org/markup-compatibility/2006"
 12: mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
 13:     
 14:     <StackPanel x:Name="LayoutRoot" Background="White">
 15:         <TextBlock Text="Введите новый текст" />
 16:         <TextBox Text="{Binding Text, Mode=TwoWay}" />
 17:         <StackPanel Orientation="Horizontal"
 18:                     HorizontalAlignment="Right">
 19:             <Button Content="OK" Command="{Binding CloseCommand}"
 20:                     Width="80" Height="30" Margin="6">
 21:                 <i:Interaction.Triggers>
 22:                     <i:EventTrigger EventName="Click">
 23:                         <ic:ChangePropertyAction
 24:                           PropertyName="ModalResult" Value="True"
 25:                           TargetObject="{Binding}" />
 26:                     </i:EventTrigger>
 27:                 </i:Interaction.Triggers>
 28:             </Button>
 29:             <Button Content="Cancel"
 30:               Command="{Binding CloseCommand}" Width="80"
 31:               Height="30" Margin="6" />
 32:         </StackPanel>
 33:     </StackPanel>
 34: </UserControl>

Здесь используется библиотека Expression Interactions, входящая в состав Microsoft Expression Blend 4, которая позволяет использовать кроссплатформенные Silverlight/WPF триггеры, схожие по функциональности с WPF триггерами, а также добавляют понятие поведения элемента управления, аналога которому в стандартной поставке WPF нет.

Далее, для работы с асинхронными моделями представления модальных дочерних окон удобно использовать библиотеку Reactive Extensions (Rx Framework):

  1: public static class ObservableHelper
  2: {
  3:     public static IObservable<EventPattern<EventArgs>>
  4:        ObserveClosed(this ICloseableViewModel childViewModel)
  5:     {
  6:         return Observable.FromEventPattern(
  7:             handler => childViewModel.Closed += handler,
  8:             handler => childViewModel.Closed -= handler);
  9:     }
 10:
 11:     public static IObservable<T> SelectSender<T>
 12:        (this IObservable<EventPattern<EventArgs>> observable)
 13:     {
 14:         return observable.Select(ev => (T)ev.Sender);
 15:     }
 16:
 17:     public static IObservable<T> WhereSucceeded<T>
 18:        (this IObservable<T> observable)
 19:         where T : IModalChildViewModel
 20:     {
 21:         return observable.Where(vm => vm.ModalResult.HasValue
 22:             vm.ModalResult.Value);
 23:     }
 24: }
  1: public static class ViewModelExtension
  2: {
  3:     /// <summary>
  4:     /// Resolves and shows Closeable View Model of
  5:     /// type <typeparamref name="T" />
  6:     /// </summary>
  7:     public static IObservable<T> ResolveAndShow<T>
  8:           (this IServiceLocator serviceLocator,
  9:            Action<T> prepareAction = null)
 10:         where T : ICloseableViewModel
 11:     {
 12:         var viewModel = serviceLocator.GetInstance<T>();
 13:
 14:         if (prepareAction != null)
 15:         {
 16:             prepareAction(viewModel);
 17:         }
 18:
 19:         IObservable<T> result = Observable.FromEventPattern
 20:               (handler => viewModel.Closed += handler,
 21:                handler => viewModel.Closed -= handler)
 22:            .SelectSender<T>();
 23:
 24:         viewModel.Show();
 25:
 26:         return result;
 27:     }
 28: }

Класс ObservableHelper позволяет, с одной стороны конструировать IObservable последовательность из события закрытия окна, и с другой стороны предоставляет удобные LINQ-подобные методы для взаимодействия с данной последовательностью. Так, метод WhereSucceeded налагает на последовательность ограничение, которое позволяет ей выполниться лишь в случае завершения диалога с положительным ModalResult (что, как правило, происходит при подтверждении какой-либо операции).

Класс ViewModelExtension представляет метод, который одним вызовом разрешает закрываемую модель представления из IoC контейнера, показывает её и возвращает IObservable последовательность её закрытия.

Используя введенные сущности, в модели представления главного окна добавляется логика отображения диалога. Для видимого эффекта использования диалога введенный в окне текст заменяет приветственный текст по умолчанию:

  1: [Export]
  2: [PartCreationPolicy(CreationPolicy.NonShared)]
  3: [ExportMetadata(AopExtensions.AspectMetadata,
  4: Aspects.NotifyPropertyChanged)]
  5: public class MainViewModel : EntitledViewModelBase
  6: {
  7:     // Private fields
  8:     private string _text = "Hello world";
  9:
 10:     public virtual string Text
 11:     {
 12:         get { return _text; }
 13:         protected set { _text = value; }
 14:     }
 15:
 16:     #region Commands
 17:
 18:     private ICommand _showTextInputCommand;
 19:
 20:     public ICommand ShowTextInputCommand
 21:     {
 22:         get
 23:         {
 24:             return _showTextInputCommand ??
 25:                     (_showTextInputCommand =
 26:                          new ActionCommand(ShowTextInput));
 27:         }
 28:     }
 29:
 30:     #endregion
 31:
 32:     #region Injected properties
 33:
 34:     [Import]
 35:     public IServiceLocator ServiceLocator { private get; set; }
 36:
 37:     #endregion
 38:
 39:     public override string Title
 40:     {
 41:         get { return "Test Application"; }
 42:     }
 43:
 44:     private void ShowTextInput()
 45:     {
 46:         ServiceLocator
 47:            .ResolveAndShow<TextInputModalChildViewModel>()
 48:            .WhereSucceeded()
 49:            .Subscribe(vm => Text = vm.Text);
 50:     }
 51: }

Соответственно, также изменяется разметка представления главного окна:

  1: <UserControl x:Class="CrossPlatformApplication.MainView"
  2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4: xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5: xmlns:mc=
  6:     "http://schemas.openxmlformats.org/markup-compatibility/2006"
  7: mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
  8:     
  9:     <Grid x:Name="LayoutRoot" Background="White"
 10:           MinHeight="150" MinWidth="200">
 11:         <TextBlock Text="{Binding Text}"
 12:                    HorizontalAlignment="Center"
 13:                    VerticalAlignment="Center" />
 14:         <Button Content="Изменить"
 15:                 Command="{Binding ShowTextInputCommand}"
 16:                 Width="80" Height="30"
 17:                 HorizontalAlignment="Right"
 18:                 VerticalAlignment="Bottom" Margin="6" />
 19:     </Grid>
 20: </UserControl>

Краткие итоги

В рамках данной лекции было написано несложное кроссплатформенное Silverlight/WPF приложение, агрегирующее все наработки предыдущих лекций. Выполненное в соответствии с шаблоном MVVM, оно стремится максимально повторно использовать как код, так и разметку, использует MEF для гибкого разрешения зависимостей между сущностями, в том числе между моделями представления и представлениями, применяет аспект INotifyPropertyChanged на модели представления, избавляя от необходимости ручного программирования логики нотификации об изменениях в каждом свойстве. При этом объем содержательного кода минимален и приложение крайне просто расширяется любым функционалом. Приведенный пример можно использовать как каркас для Silverlight/WPF приложений с богатым пользовательских интерфейсом.

Страницы:

Цель лабораторной работы: показать читателям на примере фрагментов кода полную реализацию многослойного кроссплатформенного Silverlight/WPF MVVM приложения. Данная лабораторная работа является обобщающим занятием по курсу.

Для более детального представления можно скачать исходный код итогового проекта

Создание проектов

В первую очередь необходимо создать проекты будущего приложения. В простейшем случае это будут 3 сборки: WPF Application, Silverlight Application, а также Web Application для хостинга Silverlight приложения.

Окно создание проекта вызывается нажатием пункта меню File – New – Project:

(рис 18.1) Вызов окна создания проекта

В появившемся окне необходимо перейти в раздел Other Project Types – Visual Studio Solutions и выбрать Blank Solution:

(рис 18.2) Создание пустого решения

В созданное решение следует добавить Silverlight и WPF проекты, используя контекстное меню обозревателя решений:

(рис 18.3) Добавление проекта в решение

Для добавления Silverlight приложения необходимо выбрать пункт Silverlight Application в разделе Visual C# – Silverlight:

(рис 18.4) Добавление Silverlight приложения

В появившемся диалоговом окне следует подтвердить создание Web Application для размещения Silverlight приложения и выбрать версию Silverlight:

(рис 18.5) Задание параметров Silverlight приложения

Аналогично необходимо добавить WPF приложение:

(рис 18.6) Добавление WPF приложения

Вместе с проектами были автоматически созданы главные окна, MainWindow.xaml и MainPage.xml, для WPF и Silverlight соответственно. Так как эти сущности различны в двух версиях (в случае WPF это окно, в Silverlight – UserControl), их невозможно повторно использовать. Однако в них можно поместить приведенный в лекции 11 элемент управления ViewModelPresenter, который будет делегировать создание внутренней разметки представлению, соответствующему находящейся в DataContext модели представления.

MainWindow.xaml:

  1: <Window x:Class="CrossPlatformApplication.MainWindow"
  2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4: xmlns:view="clr-namespace:CrossPlatformApplication"
  5: Title="{Binding Title}" SizeToContent="WidthAndHeight">
  6:     
  7:     <view:ViewModelPresenter ViewModel="{Binding}" />
  8: </Window>

MainPage.xaml:

  1: <UserControl x:Class="SilverlightApplication.MainPage"
  2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4: xmlns:view="clr-namespace:CrossPlatformApplication">
  5:
  6:     <view:ViewModelPresenter ViewModel="{Binding}" />
  7: </UserControl>

Согласно принципам MVVM главному представлению приложения должна соответствовать модель представления. Для начала достаточно пустого класса, общего для WPF и Silverlight:

  1: [Export]
  2: [PartCreationPolicy(CreationPolicy.NonShared)]
  3: [ExportMetadata(AopExtensions.AspectMetadata,
  4:     Aspects.NotifyPropertyChanged)]
  5: public class MainViewModel : IEntitledViewModel
  6: {
  7:     public string Title
  8:     {
  9:         get { return "Test Application"; }
 10:     }
 11: }

Затем для корректного запуска приложения необходимо добавить логику инициализации IoC контейнера MEF и MVVM окружения, а также модифицировать логику отображения главных окон. Как в WPF, так и в Silverlight приложении модифицируется код App.xaml и App.xaml.cs.

App.xaml в WPF:

  1: <Application x:Class="WpfApplication.App"
  2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  4:     <Application.Resources>
  5:          
  6:     </Application.Resources>
  7: </Application>

App.xaml.cs в WPF:

  1: public partial class App
  2: {
  3:     #region Injected properties
  4:
  5:     [Import]
  6:     public ChildViewManager ChildViewManager
  7:        { private get; set; }
  8:
  9:     #endregion
 10:
 11:     protected override void OnStartup(StartupEventArgs e)
 12:     {
 13:         base.OnStartup(e);
 14:
 15:         // Create interception configuration
 16:         var cfg = new InterceptionConfiguration()
 17:             .AddAopInterception();
 18:
 19:         var container = new CompositionContainer(
 20:            new InterceptingCatalog(new AggregateCatalog(
 21:            new DirectoryCatalog(".", "*.exe"),
 22:            new DirectoryCatalog(".", "*.dll")), cfg));
 23:
 24:         var locator = new MefServiceLocator(container);
 25:         ServiceLocator.SetLocatorProvider(() => locator);
 26:         container.ComposeExportedValue<IServiceLocator>(locator);
 27:         container.ComposeParts(this);
 28:
 29:         new MainWindow { DataContext =
 30:                   locator.GetInstance<MainViewModel>() }.Show();
 31:     }
 32: }

App.xaml в Silverlight:

  1: <Application x:Class="SilverlightApplication.App"
  2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  4:     <Application.Resources>
  5:         
  6:     </Application.Resources>
  7: </Application>

App.xaml.cs в Silverlight:

  1: public partial class App
  2: {
  3:     public App()
  4:     {
  5:         Startup += Application_Startup;
  6:         Exit += Application_Exit;
  7:         UnhandledException += Application_UnhandledException;
  8:
  9:         InitializeComponent();
 10:     }
 11:
 12:     #region Injected properties
 13:
 14:     [Import]
 15:     public ChildViewManager ChildViewManager
 16:        { private get; set; }
 17:
 18:     #endregion
 19:
 20:     private void Application_Startup
 21:        (object sender, StartupEventArgs e)
 22:     {
 23:         // Create interception configuration
 24:         var cfg = new InterceptionConfiguration()
 25:             .AddAopInterception();
 26:
 27:         var container = new CompositionContainer(
 28:                         new InterceptingCatalog(
 29:                         new DeploymentCatalog(), cfg));
 30:         var locator = new MefServiceLocator(container);
 31:         ServiceLocator.SetLocatorProvider(() => locator);
 32:         container.ComposeExportedValue<IServiceLocator>(locator);
 33:         container.ComposeParts(this);
 34:
 35:         RootVisual = new MainPage
 36:           { DataContext = locator.GetInstance<MainViewModel>() };
 37:     }
 38:
 39:     private void Application_Exit(object sender, EventArgs e)
 40:     {
 41:
 42:     }
 43:
 44:     private void Application_UnhandledException
 46:        (object sender, ApplicationUnhandledExceptionEventArgs e)
 47:     {
 48:         if (!System.Diagnostics.Debugger.IsAttached)
 49:         {
 50:             e.Handled = true;
 51:             Deployment.Current.Dispatcher.BeginInvoke(
 52:                delegate { ReportErrorToDOM(e); });
 53:         }
 54:     }
 55:
 56:     private void ReportErrorToDOM
 57:        (ApplicationUnhandledExceptionEventArgs e)
 44:     {
 45:         try
 46:         {
 47:             string errorMsg = e.ExceptionObject.Message
 48:                + e.ExceptionObject.StackTrace;
 49:             errorMsg = errorMsg.Replace('"',
 50:                '\'').Replace("\r\n", @"\n");
 51:
 52:             System.Windows.Browser.HtmlPage.Window.Eval(
 53:   "throw new Error(\"Unhandled Error in Silverlight Application "
 54:   + errorMsg + "\");");
 55:         }
 56:         catch (Exception)
 57:         {
 58:         }
 59:     }
 60: }

Как видно из кода, главные окна приложения будут отображать представление MainView, соответствующее модели представления MainViewModel. Пусть это представление будет содержать простой текст "Hello world":

  1: <UserControl x:Class="CrossPlatformApplication.MainView"
  2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4: xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5: xmlns:mc
  6:    ="http://schemas.openxmlformats.org/markup-compatibility/2006"
  7: mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
  8:     
  9:     <Grid x:Name="LayoutRoot" Background="White" MinHeight="150"
 10:           MinWidth="200">
 11:         <TextBlock Text="Hello world"
 12:                    HorizontalAlignment="Center"
 13:                    VerticalAlignment="Center" />
 14:     </Grid>
 15: </UserControl>

Естественно, как говорилось в лекции 12, необходимо создать представление в Silverlight проекте и добавить ссылку на него в WPF проект. Теперь оба приложения готовы к запуску и отображают на своем главном окне сообщение "Hello world".

Следующим этапом следует некоторое усложнение примера – размещенная на главном представлении кнопка будет отображать модальное дочернее окно, считывающее строку у пользователя. Введенные данные модифицируют исходный текст на главном представлении.

В первую очередь необходимо создать модель представления диалога, которая наследуется от класса ModalViewModelBase:

  1: [Export]
  2: [PartCreationPolicy(CreationPolicy.NonShared)]
  3: [ExportMetadata(AopExtensions.AspectMetadata,
  4:    Aspects.NotifyPropertyChanged)]
  5: public class TextInputModalChildViewModel :
  6:    ModalChildViewModelBase
  7: {
  8:     public virtual string Text { get; set; }
  9: }

Далее создается представление, содержащее поле ввода, привязанное к свойству Text:

  1: <UserControl
  2: x:Class="CrossPlatformApplication.TextInputModalChildView"
  3: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  5: xmlns:i=
  6:    "http://schemas.microsoft.com/expression/2010/interactivity"
  7: xmlns:ic=
  8:    "http://schemas.microsoft.com/expression/2010/interactions"
  9: xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 10: xmlns:mc=
 11:    "http://schemas.openxmlformats.org/markup-compatibility/2006"
 12: mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
 13:     
 14:     <StackPanel x:Name="LayoutRoot" Background="White">
 15:         <TextBlock Text="Введите новый текст" />
 16:         <TextBox Text="{Binding Text, Mode=TwoWay}" />
 17:         <StackPanel Orientation="Horizontal"
 18:                     HorizontalAlignment="Right">
 19:             <Button Content="OK" Command="{Binding CloseCommand}"
 20:                     Width="80" Height="30" Margin="6">
 21:                 <i:Interaction.Triggers>
 22:                     <i:EventTrigger EventName="Click">
 23:                         <ic:ChangePropertyAction
 24:                           PropertyName="ModalResult" Value="True"
 25:                           TargetObject="{Binding}" />
 26:                     </i:EventTrigger>
 27:                 </i:Interaction.Triggers>
 28:             </Button>
 29:             <Button Content="Cancel"
 30:               Command="{Binding CloseCommand}" Width="80"
 31:               Height="30" Margin="6" />
 32:         </StackPanel>
 33:     </StackPanel>
 34: </UserControl>

Здесь используется библиотека Expression Interactions, входящая в состав Microsoft Expression Blend 4, которая позволяет использовать кроссплатформенные Silverlight/WPF триггеры, схожие по функциональности с WPF триггерами, а также добавляют понятие поведения элемента управления, аналога которому в стандартной поставке WPF нет.

Далее, для работы с асинхронными моделями представления модальных дочерних окон удобно использовать библиотеку Reactive Extensions (Rx Framework):

  1: public static class ObservableHelper
  2: {
  3:     public static IObservable<EventPattern<EventArgs>>
  4:        ObserveClosed(this ICloseableViewModel childViewModel)
  5:     {
  6:         return Observable.FromEventPattern(
  7:             handler => childViewModel.Closed += handler,
  8:             handler => childViewModel.Closed -= handler);
  9:     }
 10:
 11:     public static IObservable<T> SelectSender<T>
 12:        (this IObservable<EventPattern<EventArgs>> observable)
 13:     {
 14:         return observable.Select(ev => (T)ev.Sender);
 15:     }
 16:
 17:     public static IObservable<T> WhereSucceeded<T>
 18:        (this IObservable<T> observable)
 19:         where T : IModalChildViewModel
 20:     {
 21:         return observable.Where(vm => vm.ModalResult.HasValue
 22:             vm.ModalResult.Value);
 23:     }
 24: }
  1: public static class ViewModelExtension
  2: {
  3:     /// <summary>
  4:     /// Resolves and shows Closeable View Model of
  5:     /// type <typeparamref name="T" />
  6:     /// </summary>
  7:     public static IObservable<T> ResolveAndShow<T>
  8:           (this IServiceLocator serviceLocator,
  9:            Action<T> prepareAction = null)
 10:         where T : ICloseableViewModel
 11:     {
 12:         var viewModel = serviceLocator.GetInstance<T>();
 13:
 14:         if (prepareAction != null)
 15:         {
 16:             prepareAction(viewModel);
 17:         }
 18:
 19:         IObservable<T> result = Observable.FromEventPattern
 20:               (handler => viewModel.Closed += handler,
 21:                handler => viewModel.Closed -= handler)
 22:            .SelectSender<T>();
 23:
 24:         viewModel.Show();
 25:
 26:         return result;
 27:     }
 28: }

Класс ObservableHelper позволяет, с одной стороны конструировать IObservable последовательность из события закрытия окна, и с другой стороны предоставляет удобные LINQ-подобные методы для взаимодействия с данной последовательностью. Так, метод WhereSucceeded налагает на последовательность ограничение, которое позволяет ей выполниться лишь в случае завершения диалога с положительным ModalResult (что, как правило, происходит при подтверждении какой-либо операции).

Класс ViewModelExtension представляет метод, который одним вызовом разрешает закрываемую модель представления из IoC контейнера, показывает её и возвращает IObservable последовательность её закрытия.

Используя введенные сущности, в модели представления главного окна добавляется логика отображения диалога. Для видимого эффекта использования диалога введенный в окне текст заменяет приветственный текст по умолчанию:

  1: [Export]
  2: [PartCreationPolicy(CreationPolicy.NonShared)]
  3: [ExportMetadata(AopExtensions.AspectMetadata,
  4: Aspects.NotifyPropertyChanged)]
  5: public class MainViewModel : EntitledViewModelBase
  6: {
  7:     // Private fields
  8:     private string _text = "Hello world";
  9:
 10:     public virtual string Text
 11:     {
 12:         get { return _text; }
 13:         protected set { _text = value; }
 14:     }
 15:
 16:     #region Commands
 17:
 18:     private ICommand _showTextInputCommand;
 19:
 20:     public ICommand ShowTextInputCommand
 21:     {
 22:         get
 23:         {
 24:             return _showTextInputCommand ??
 25:                     (_showTextInputCommand =
 26:                          new ActionCommand(ShowTextInput));
 27:         }
 28:     }
 29:
 30:     #endregion
 31:
 32:     #region Injected properties
 33:
 34:     [Import]
 35:     public IServiceLocator ServiceLocator { private get; set; }
 36:
 37:     #endregion
 38:
 39:     public override string Title
 40:     {
 41:         get { return "Test Application"; }
 42:     }
 43:
 44:     private void ShowTextInput()
 45:     {
 46:         ServiceLocator
 47:            .ResolveAndShow<TextInputModalChildViewModel>()
 48:            .WhereSucceeded()
 49:            .Subscribe(vm => Text = vm.Text);
 50:     }
 51: }

Соответственно, также изменяется разметка представления главного окна:

  1: <UserControl x:Class="CrossPlatformApplication.MainView"
  2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4: xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5: xmlns:mc=
  6:     "http://schemas.openxmlformats.org/markup-compatibility/2006"
  7: mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
  8:     
  9:     <Grid x:Name="LayoutRoot" Background="White"
 10:           MinHeight="150" MinWidth="200">
 11:         <TextBlock Text="{Binding Text}"
 12:                    HorizontalAlignment="Center"
 13:                    VerticalAlignment="Center" />
 14:         <Button Content="Изменить"
 15:                 Command="{Binding ShowTextInputCommand}"
 16:                 Width="80" Height="30"
 17:                 HorizontalAlignment="Right"
 18:                 VerticalAlignment="Bottom" Margin="6" />
 19:     </Grid>
 20: </UserControl>

Краткие итоги

В рамках данной лекции было написано несложное кроссплатформенное Silverlight/WPF приложение, агрегирующее все наработки предыдущих лекций. Выполненное в соответствии с шаблоном MVVM, оно стремится максимально повторно использовать как код, так и разметку, использует MEF для гибкого разрешения зависимостей между сущностями, в том числе между моделями представления и представлениями, применяет аспект INotifyPropertyChanged на модели представления, избавляя от необходимости ручного программирования логики нотификации об изменениях в каждом свойстве. При этом объем содержательного кода минимален и приложение крайне просто расширяется любым функционалом. Приведенный пример можно использовать как каркас для Silverlight/WPF приложений с богатым пользовательских интерфейсом.

Вернуться к учебному плану