Построение кроссплатформенного Silverlight/WPF приложения
В первую очередь необходимо создать модель представления диалога, которая наследуется от класса 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 приложений с богатым пользовательских интерфейсом.