Разработка WPF приложений
в стиле ViewModel First
Денис Цветцих
АстроСофт
http://www.astrosoft.ru/
11-я конференция .NET разработчиков
31 октября 2015
dotnetconf.ru
2
Почему это актуально
• WPF все ещё жив 
• MVVM – тема множества докладов и статей
• Множество разных реализаций
• от MVVM.Light
• до Prism с мануалом на 250 страниц
3
В чем проблема?
Много разных реализаций MVVM
• Непонятно, чем они отличаются
• Непонятно, какую из них использовать и
в каких случаях
4
Почему я здесь
• Накоплен интересный опыт участия в WPF
проектах
• Есть опыт, связанный с фреймворками:
• Использование сторонних
• Доработка сторонних
• Изобретение своего MVVM фреймворка 
5
Опрос
• Кто собирается написать WPF приложение?
• Кто при этом использовал какие-нибудь
MVVM библиотеки/фреймворки?
• Кто их вас изобрел свой собственный
MVVM велосипед фреймворк?
6
О чем мы поговорим?
• Что такое MVVM?
• Какими бывают подходы к его реализации?
• Как отличаются организация CompositeUI и
дочерних окон в разных подходах?
• Какой подход лучше использовать?
• Где найти реализацию этого подхода?
• Рекомендации на основе нашего опыта
7
Model-View-ViewModel
8
Что со всем этим делать?
Как решать типовые задачи?
• CompositeUI (MasterDetail форма)
• Навигация (дочерние окна)
9
Нам нужен MVVM фреймворк!
Бывает 2 типов:
• ViewFirst
• ViewModelFirst
Это не разные реализации паттерна MVVM
Это разные подходы к решению типовых задач с
использованием MVVM
COMPOSITE UI
11
Master Detail
12
ViewFirst (Prism)
Отображение нового региона:
1) Создать View
2) Создать ViewModel для View
3) View.DataContext = ViewModel
4) Инициализировать ViewModel
13
MainWinow
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<view:UserListView Grid.Column="0" />
<view:UserDetailsView Grid.Column="1" />
</Grid>
14
UserListView
<Grid>
<TextBlock Grid.Row="0" Text="User list" FontWeight="Bold" />
<ListBox Grid.Row="1" x:Name="UserListBox"
SelectedItem="{Binding SelectedUser, Mode=TwoWay}"
ItemsSource="{Binding Users}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FirstName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
public UserListView()
{
DataContext = new UserListViewModel();
}
15
UserDetailsView
<StackPanel Orientation="Vertical">
<TextBlock Text="User details" FontWeight="Bold" />
<TextBlock Text="{Binding User.FirstName}" />
<TextBlock Text="{Binding User.LastName}" />
</StackPanel>
public UserDetailsView()
{
DataContext = new UserDetailsViewModel();
}
16
UserListViewModel
public class UserListViewModel : ViewModel
{
private User _selectedUser;
public IEnumerable<User> Users { get; } // инициализация
public User SelectedUser
{
get { return _selectedUser; }
set
{
_selectedUser = value;
OnPropertyChanged();
}
}
}
17
UserDetailsViewModel
public class UserDetailsViewModel : ViewModel
{
private User _user;
public User User
{
get { return _user; }
set
{
_user = value;
OnPropertyChanged();
}
}
}
18
Вопрос
Как сделать так, чтобы
UserListViewModel.SelectedUser
синхронизировалось с
UserDetailsViewModel.User?
Ответ в стиле ViewFirst – MessageBus
19
MessageBus
20
MessageBus
public class MessageBus
{
public static MessageBus Instance = new MessageBus();
public event EventHandler<UserChangedEventArgs> SelectedUserChanged;
public void OnSelectedUserChanged(User user)
{
SelectedUserChanged?.Invoke(this, new UserChangedEventArgs(user));
}
}
public class UserChangedEventArgs : EventArgs
{
public UserChangedEventArgs(User user)
{
User = user;
}
public User User { get; }
}
21
UserListViewModel
public class UserListViewModel : ViewModel
{
public User SelectedUser
{
get { return _selectedUser; }
set
{
_selectedUser = value;
OnPropertyChanged();
MessageBus.Instance.OnSelectedUserChanged(value);
}
}
}
22
UserDetailsViewModel
public class UserDetailsViewModel : ViewModel
{
public UserDetailsViewModel()
{
MessageBus.Instance.SelectedUserChanged +=
(s, e) => User = e.User;
}
}
23
Недостатки
1) Используется MessageBus,
предназначенный для интеграции систем
2) На широковещательное событие может
подписаться любой объект
3) Поведение системы становится
запутанным и неочевидным
24
ViewModelFirst (энтузиасты)
Отображение нового региона:
1) Создать ViewModel
2) Инициализировать ViewModel
3) Создать View для ViewModel
4) View.DataContext = ViewModel
25
Чистим CodeBehind
public UserListView()
{
DataContext = new UserListViewModel();
}
public UserDetailsView()
{
DataContext = new UserDetailsViewModel();
}
26
Убираем MessageBus
public class UserListViewModel : ViewModel
{
public User SelectedUser
{
get { return _selectedUser; }
set
{
_selectedUser = value;
OnPropertyChanged();
MessageBus.Instance.OnSelectedUserChanged(value);
}
}
}
27
Убираем MessageBus
public class UserDetailsViewModel : ViewModel
{
public UserDetailsViewModel()
{
MessageBus.Instance.SelectedUserChanged +=
(s, e) => User = e.User;
}
}
28
Вопрос
Как сделать так, чтобы
UserListViewModel.SelectedUser
синхронизировалось с
UserDetailsViewModel.User?
Ответ в стиле ViewModelFirst – нам нужна
родительская ViewModel
29
MainWindowViewModel
public class MainWindowViewModel : ViewModel
{
public UserDetailsViewModel UserDetailsViewModel { get; private set; }
public UserListViewModel UserListViewModel { get; private set; }
public void Initialize()
{
UserListViewModel.PropertyChanged += (s, e) =>
{
if (e.PropertyName == "SelectedUser")
UserDetailsViewModel.User = UserListViewModel.SelectedUser;
};
}
}
30
MainWindow
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<views:UserListView
DataContext="{Binding UserListViewModel}"
Grid.Column="0" />
<views:UserDetailsView
DataContext="{Binding UserDetailsViewModel}"
Grid.Column="1" />
</Grid>
31
ViewFirst vs ViewModelFirst
ViewFirst ViewModelFitst
CompositeUI да да
Взаимодействие
ViewModel
MessageBus Родетельская
ViewModel
НАВИГАЦИЯ
33
ViewFirst: показать новый элемент
Дочернее окно, новый таб:
Navigation.Show<ViewModel>(Value);
или
Navigation.Show("View", Value);
Аналогично вебу: http://address.ru/?arg=value
ViewFirst предлагает в WPF организовать навигацию
аналогично веб-приложению
34
ViewModelFirst: показать новый элемент
var vm = Navigation.Get<ViewModel>();
vm.Arg = Value;
vm.Show();
Аналогично окну WPF:
var wnd = new Window();
wnd.Arg1 = Value1;
wnd.Show();
35
ViewFirst vs ViewModelFirst
ViewFirst ViewModelFirst
Создать View Создать ViewModel
Создать ViewModel Инициализировать ViewModel
Инициализировать ViewModel Создать View
View.DataContext = ViewModel View.DataContext = ViewModel
36
Дочернее окно
37
Отображение дочернего окна
private void OnShowDetails(User user)
{
var detailsVM =
Factory.Resolve<UserDetailsWindowViewModel>();
detailsVM.User = user;
detailsVM.Closed +=
(s, e) => { /* обработка e.DialogResult */ };
detailsVM.Show();
}
38
ChildViewModel
public abstract class ChildViewModel : ViewModel, IChildViewModel
{
[Dependency]
public IChildViewModelManager ChildViewModelManager { private get; set; }
public bool IsClosed { get; private set; } // уже закрыли или нет?
protected void Close()
{
if (IsClosed) throw new InvalidOperationException(“closed");
IsClosed = true;
ChildViewModelManager.Close(this);
}
public void Show()
{
ChildViewModelManager.Show(this);
}
}
39
ChildViewModelManager
public class ChildViewModelManager : IChildViewModelManager
{
// открытые окна
private readonly Dictionary<Type, Window> _openedWindows
= new Dictionary<Type, Window>();
// по типу ViewModel возвращает View
[Dependency]
public IViewTypeResolver ViewTypeResolver
{ private get; set; }
}
40
ChildViewModelManager: Show
private void Show(IChildViewModel viewModel)
{
// получить тип окна, которое будем открывать
var windowType = ViewTypeResolver.
ResolveViewType(viewModel.GetType());
// создать экземпляр окна
var window =
(Window)Activator.CreateInstance(windowType);
// запомнить, какое окно открываем
_openedWindows.Add(viewModel.GetType(), window);
window.DataContext = viewModel;
// показать окно
window.Show();
}
41
ChildViewModelManager: Close
public void Close(IChildViewModel viewModel)
{
// какое окно закрываем
var window = _openedWindows[viewModel.GetType()];
// убираем из списка открытых
_openedWindows.Remove(viewModel.GetType());
// закрываем
Application.Current.Dispatcher.BeginInvoke(
new Action(() => window.Close()), null);
}
42
ChildWindow
public abstract class ChildWindow : Window
{
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
var viewModel = (ChildViewModel)DataContext;
// если ViewModel на находится в состоянии «Закрыто»
if (!viewModel.IsClosed)
{
e.Cancel = true; // не закрываем окно
// запрашиваем изменение состояния ViewModel
viewModel.Close();
}
}
}
43
Особенности ViewModelFirst
Достоинства
• Позволяет реализовать CompositeUI
• Не требует реализации MessageBus
• Взаимодействие ViewModel более очевидное
• Нет MessageBus – нет его использования не
по назначению
• Позволяет удобно реализовать
поддержку дочерних окон
Недостатки
• Не имеет вендорской поддержки
44
Наш рецепт
• ViewModelFirst
• Свой велосипед
• Mugen MVVM Toolkit
• IoC контейнер
• ReactiveUI (ограничено)
• ReactiveCommand
• ObservableForProperty
• Отдельная сборка для ViewModel
45
Материалы по ViewModelFirst
Материалы доклада на GitHub:
https://github.com/denis-tsv/ViewFirst-vs-ViewModelFirst
Курс «Методология синхронной разработки
приложений в Microsoft Visual Studio 2010»
www.intuit.ru/studies/courses/2322/622/info
Mugen MVVM Toolkit
http://habrahabr.ru/post/236745/
46
Спасибо за внимание
Денис Цветцих
АстроСофт
den.tsvettsih@yandex.ru

Разработка WPF приложений в стиле ViewModel First

  • 1.
    Разработка WPF приложений встиле ViewModel First Денис Цветцих АстроСофт http://www.astrosoft.ru/ 11-я конференция .NET разработчиков 31 октября 2015 dotnetconf.ru
  • 2.
    2 Почему это актуально •WPF все ещё жив  • MVVM – тема множества докладов и статей • Множество разных реализаций • от MVVM.Light • до Prism с мануалом на 250 страниц
  • 3.
    3 В чем проблема? Многоразных реализаций MVVM • Непонятно, чем они отличаются • Непонятно, какую из них использовать и в каких случаях
  • 4.
    4 Почему я здесь •Накоплен интересный опыт участия в WPF проектах • Есть опыт, связанный с фреймворками: • Использование сторонних • Доработка сторонних • Изобретение своего MVVM фреймворка 
  • 5.
    5 Опрос • Кто собираетсянаписать WPF приложение? • Кто при этом использовал какие-нибудь MVVM библиотеки/фреймворки? • Кто их вас изобрел свой собственный MVVM велосипед фреймворк?
  • 6.
    6 О чем мыпоговорим? • Что такое MVVM? • Какими бывают подходы к его реализации? • Как отличаются организация CompositeUI и дочерних окон в разных подходах? • Какой подход лучше использовать? • Где найти реализацию этого подхода? • Рекомендации на основе нашего опыта
  • 7.
  • 8.
    8 Что со всемэтим делать? Как решать типовые задачи? • CompositeUI (MasterDetail форма) • Навигация (дочерние окна)
  • 9.
    9 Нам нужен MVVMфреймворк! Бывает 2 типов: • ViewFirst • ViewModelFirst Это не разные реализации паттерна MVVM Это разные подходы к решению типовых задач с использованием MVVM
  • 10.
  • 11.
  • 12.
    12 ViewFirst (Prism) Отображение новогорегиона: 1) Создать View 2) Создать ViewModel для View 3) View.DataContext = ViewModel 4) Инициализировать ViewModel
  • 13.
  • 14.
    14 UserListView <Grid> <TextBlock Grid.Row="0" Text="Userlist" FontWeight="Bold" /> <ListBox Grid.Row="1" x:Name="UserListBox" SelectedItem="{Binding SelectedUser, Mode=TwoWay}" ItemsSource="{Binding Users}"> <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding FirstName}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> public UserListView() { DataContext = new UserListViewModel(); }
  • 15.
    15 UserDetailsView <StackPanel Orientation="Vertical"> <TextBlock Text="Userdetails" FontWeight="Bold" /> <TextBlock Text="{Binding User.FirstName}" /> <TextBlock Text="{Binding User.LastName}" /> </StackPanel> public UserDetailsView() { DataContext = new UserDetailsViewModel(); }
  • 16.
    16 UserListViewModel public class UserListViewModel: ViewModel { private User _selectedUser; public IEnumerable<User> Users { get; } // инициализация public User SelectedUser { get { return _selectedUser; } set { _selectedUser = value; OnPropertyChanged(); } } }
  • 17.
    17 UserDetailsViewModel public class UserDetailsViewModel: ViewModel { private User _user; public User User { get { return _user; } set { _user = value; OnPropertyChanged(); } } }
  • 18.
    18 Вопрос Как сделать так,чтобы UserListViewModel.SelectedUser синхронизировалось с UserDetailsViewModel.User? Ответ в стиле ViewFirst – MessageBus
  • 19.
  • 20.
    20 MessageBus public class MessageBus { publicstatic MessageBus Instance = new MessageBus(); public event EventHandler<UserChangedEventArgs> SelectedUserChanged; public void OnSelectedUserChanged(User user) { SelectedUserChanged?.Invoke(this, new UserChangedEventArgs(user)); } } public class UserChangedEventArgs : EventArgs { public UserChangedEventArgs(User user) { User = user; } public User User { get; } }
  • 21.
    21 UserListViewModel public class UserListViewModel: ViewModel { public User SelectedUser { get { return _selectedUser; } set { _selectedUser = value; OnPropertyChanged(); MessageBus.Instance.OnSelectedUserChanged(value); } } }
  • 22.
    22 UserDetailsViewModel public class UserDetailsViewModel: ViewModel { public UserDetailsViewModel() { MessageBus.Instance.SelectedUserChanged += (s, e) => User = e.User; } }
  • 23.
    23 Недостатки 1) Используется MessageBus, предназначенныйдля интеграции систем 2) На широковещательное событие может подписаться любой объект 3) Поведение системы становится запутанным и неочевидным
  • 24.
    24 ViewModelFirst (энтузиасты) Отображение новогорегиона: 1) Создать ViewModel 2) Инициализировать ViewModel 3) Создать View для ViewModel 4) View.DataContext = ViewModel
  • 25.
    25 Чистим CodeBehind public UserListView() { DataContext= new UserListViewModel(); } public UserDetailsView() { DataContext = new UserDetailsViewModel(); }
  • 26.
    26 Убираем MessageBus public classUserListViewModel : ViewModel { public User SelectedUser { get { return _selectedUser; } set { _selectedUser = value; OnPropertyChanged(); MessageBus.Instance.OnSelectedUserChanged(value); } } }
  • 27.
    27 Убираем MessageBus public classUserDetailsViewModel : ViewModel { public UserDetailsViewModel() { MessageBus.Instance.SelectedUserChanged += (s, e) => User = e.User; } }
  • 28.
    28 Вопрос Как сделать так,чтобы UserListViewModel.SelectedUser синхронизировалось с UserDetailsViewModel.User? Ответ в стиле ViewModelFirst – нам нужна родительская ViewModel
  • 29.
    29 MainWindowViewModel public class MainWindowViewModel: ViewModel { public UserDetailsViewModel UserDetailsViewModel { get; private set; } public UserListViewModel UserListViewModel { get; private set; } public void Initialize() { UserListViewModel.PropertyChanged += (s, e) => { if (e.PropertyName == "SelectedUser") UserDetailsViewModel.User = UserListViewModel.SelectedUser; }; } }
  • 30.
    30 MainWindow <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <views:UserListView DataContext="{BindingUserListViewModel}" Grid.Column="0" /> <views:UserDetailsView DataContext="{Binding UserDetailsViewModel}" Grid.Column="1" /> </Grid>
  • 31.
    31 ViewFirst vs ViewModelFirst ViewFirstViewModelFitst CompositeUI да да Взаимодействие ViewModel MessageBus Родетельская ViewModel
  • 32.
  • 33.
    33 ViewFirst: показать новыйэлемент Дочернее окно, новый таб: Navigation.Show<ViewModel>(Value); или Navigation.Show("View", Value); Аналогично вебу: http://address.ru/?arg=value ViewFirst предлагает в WPF организовать навигацию аналогично веб-приложению
  • 34.
    34 ViewModelFirst: показать новыйэлемент var vm = Navigation.Get<ViewModel>(); vm.Arg = Value; vm.Show(); Аналогично окну WPF: var wnd = new Window(); wnd.Arg1 = Value1; wnd.Show();
  • 35.
    35 ViewFirst vs ViewModelFirst ViewFirstViewModelFirst Создать View Создать ViewModel Создать ViewModel Инициализировать ViewModel Инициализировать ViewModel Создать View View.DataContext = ViewModel View.DataContext = ViewModel
  • 36.
  • 37.
    37 Отображение дочернего окна privatevoid OnShowDetails(User user) { var detailsVM = Factory.Resolve<UserDetailsWindowViewModel>(); detailsVM.User = user; detailsVM.Closed += (s, e) => { /* обработка e.DialogResult */ }; detailsVM.Show(); }
  • 38.
    38 ChildViewModel public abstract classChildViewModel : ViewModel, IChildViewModel { [Dependency] public IChildViewModelManager ChildViewModelManager { private get; set; } public bool IsClosed { get; private set; } // уже закрыли или нет? protected void Close() { if (IsClosed) throw new InvalidOperationException(“closed"); IsClosed = true; ChildViewModelManager.Close(this); } public void Show() { ChildViewModelManager.Show(this); } }
  • 39.
    39 ChildViewModelManager public class ChildViewModelManager: IChildViewModelManager { // открытые окна private readonly Dictionary<Type, Window> _openedWindows = new Dictionary<Type, Window>(); // по типу ViewModel возвращает View [Dependency] public IViewTypeResolver ViewTypeResolver { private get; set; } }
  • 40.
    40 ChildViewModelManager: Show private voidShow(IChildViewModel viewModel) { // получить тип окна, которое будем открывать var windowType = ViewTypeResolver. ResolveViewType(viewModel.GetType()); // создать экземпляр окна var window = (Window)Activator.CreateInstance(windowType); // запомнить, какое окно открываем _openedWindows.Add(viewModel.GetType(), window); window.DataContext = viewModel; // показать окно window.Show(); }
  • 41.
    41 ChildViewModelManager: Close public voidClose(IChildViewModel viewModel) { // какое окно закрываем var window = _openedWindows[viewModel.GetType()]; // убираем из списка открытых _openedWindows.Remove(viewModel.GetType()); // закрываем Application.Current.Dispatcher.BeginInvoke( new Action(() => window.Close()), null); }
  • 42.
    42 ChildWindow public abstract classChildWindow : Window { protected override void OnClosing(CancelEventArgs e) { base.OnClosing(e); var viewModel = (ChildViewModel)DataContext; // если ViewModel на находится в состоянии «Закрыто» if (!viewModel.IsClosed) { e.Cancel = true; // не закрываем окно // запрашиваем изменение состояния ViewModel viewModel.Close(); } } }
  • 43.
    43 Особенности ViewModelFirst Достоинства • Позволяетреализовать CompositeUI • Не требует реализации MessageBus • Взаимодействие ViewModel более очевидное • Нет MessageBus – нет его использования не по назначению • Позволяет удобно реализовать поддержку дочерних окон Недостатки • Не имеет вендорской поддержки
  • 44.
    44 Наш рецепт • ViewModelFirst •Свой велосипед • Mugen MVVM Toolkit • IoC контейнер • ReactiveUI (ограничено) • ReactiveCommand • ObservableForProperty • Отдельная сборка для ViewModel
  • 45.
    45 Материалы по ViewModelFirst Материалыдоклада на GitHub: https://github.com/denis-tsv/ViewFirst-vs-ViewModelFirst Курс «Методология синхронной разработки приложений в Microsoft Visual Studio 2010» www.intuit.ru/studies/courses/2322/622/info Mugen MVVM Toolkit http://habrahabr.ru/post/236745/
  • 46.
    46 Спасибо за внимание ДенисЦветцих АстроСофт den.tsvettsih@yandex.ru

Editor's Notes

  • #2 Рассказать что было много WPF проектов, было набито много шишек, использованы чужие MVVM. Доработаны чужие, изобретены свои. Есть опыт, которым хочется поделиться. История про красноярский и новгородский офис
  • #9 Добавить картинок