outcoldman
outcoldman Denis Gladkikh

Еще одно сравнение паттернов MVVM и MVP для Silverlight

Silverlight, MVP, MVVM, Silverlight 4, and Bug

Я уже когда-то поднимал тему сравнения паттернов MVP и MVVM при разработке Silverlight приложений: Выступление на ADD2010: Silverlight/WPF: возврат от паттерна MVVM к MVP. Вопрос, на самом деле сложный, какой из паттернов лучше. Я бы хотел продемонстрировать небольшой пример и подискутировать на эту тему в комментариях. Был бы рад, если кто-нибудь нашел бы хороший ответ на мой вопрос, который будет дальше.

Давайте рассмотрим простой пример. Сделаем ContextMenu в TextBox с одним MenuItem, которое будет просто вызывать MessageBox. Сразу оговорюсь, что сделать я хочу именно так, как сделаю. Вторым примером сделаем ListBox с элементами TextBox, где у каждого будет тоже свое ContextMenu. 

Пример 1. Реализация с паттерном MVVM

Реализуем простейшую команду:

public class MyCommand : ICommand
{    public bool CanExecute(object parameter)
    {        return true;
    }
     public void Execute(object parameter)
    {        MessageBox.Show("Yep!");
    }
     public event EventHandler CanExecuteChanged;
}

Делаем простейший ViewModel:

public class ViewModel
{    private MyCommand _command = new MyCommand();
     public MyCommand Command { get { return _command; } }
}

Пишем разметку:

<UserControl x:Class="SilverlightApplication3.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:SilverlightApplication3="clr-namespace:SilverlightApplication3" 
    xmlns:Controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit" 
    x:Name="ParentControl">
     <UserControl.DataContext>
        <SilverlightApplication3:ViewModel />
    </UserControl.DataContext>
    <Grid x:Name="LayoutRoot" Background="White">
         <TextBox HorizontalAlignment="Center" VerticalAlignment="Center" Width="300" Height="200" >
            <Controls:ContextMenuService.ContextMenu>
                <Controls:ContextMenu>
                    <Controls:MenuItem Header="Test" Command="{Binding Path=DataContext.Command, ElementName=ParentControl}"  />
                </Controls:ContextMenu>
            </Controls:ContextMenuService.ContextMenu>
        </TextBox>
     </Grid>
</UserControl>

В code behind все по-стандартному. Задача вырвана из контекста, потому у меня тут немного странный байндинг. Ну к нему можно прийти очень даже просто, если Grid, например, будет иметь DataContext={Binding Path=Entity} или что-то типа того.

Пример 1. Реализация при помощи паттерна MVP

Меняем разметку:

<UserControl x:Class="SilverlightApplication3.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit" >
     <Grid x:Name="LayoutRoot" Background="White">
         <TextBox HorizontalAlignment="Center" VerticalAlignment="Center" Width="300" Height="200" >
            <Controls:ContextMenuService.ContextMenu>
                <Controls:ContextMenu>
                    <Controls:MenuItem Header="Test" Click="MenuItem_Click"  />
                </Controls:ContextMenu>
            </Controls:ContextMenuService.ContextMenu>
        </TextBox>
     </Grid>
</UserControl>

Создаем класс Presenter:

public class Presenter
{    public void DoAction()
    {        MessageBox.Show("Yep!");
    }
}

И немного кода в code behind самой View:

public partial class MainPage : UserControl
{    private Presenter _presenter = new Presenter();
     public MainPage()
    {
        InitializeComponent();
    }
     private void MenuItem_Click(object sender, RoutedEventArgs e)
    {
        _presenter.DoAction();
    }
}

Пример 2. Реализация с паттерном MVVM

Здесь у нас уже будет коллекция элементов, на основе которой будет,  в принципе, то же самое, что в примере 1. Код:

public class Entity
{    public string Title { get; set; }
}
 public class MyCommand : ICommand
{    public bool CanExecute(object parameter)
    {        return true;
    }
     public void Execute(object parameter)
    {        MessageBox.Show("Yep!");
    }
     public event EventHandler CanExecuteChanged;
}
 public class ViewModel
{    private List<Entity> _entities = new List<Entity>() {
        new Entity() {Title = "Entity 1"},
        new Entity() {Title = "Entity 2"},
        new Entity() {Title = "Entity 3"}
    };
     private MyCommand _command = new MyCommand();
     public MyCommand Command
    {        get { return _command; }
    }
     public List<Entity> Entities
    {        get { return _entities; }
    }
}

Я тут сделал класс Entity с одним свойством, а во ViewModel добавил свойство, которое возвращает коллекцию Entities. XAML меняется следующим образом:

<UserControl x:Class="SilverlightApplication3.MainPage"     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"     
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     
             xmlns:SilverlightApplication3="clr-namespace:SilverlightApplication3"      
             xmlns:Controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit"      
             x:Name="ParentControl">
    <UserControl.DataContext>
        <SilverlightApplication3:ViewModel />
    </UserControl.DataContext>
    <Grid x:Name="LayoutRoot" Background="White">
        <ListBox HorizontalAlignment="Center" VerticalAlignment="Center" ItemsSource="{Binding Path=Entities}" >
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBox Width="300" Height="100" >
                        <Controls:ContextMenuService.ContextMenu>
                            <Controls:ContextMenu>
                                <Controls:MenuItem Header="Test" Command="{Binding Path=DataContext.Command, ElementName=LayoutRoot}" />
                            </Controls:ContextMenu>
                        </Controls:ContextMenuService.ContextMenu>
                    </TextBox>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</UserControl>

Пример 2. Реализация при помощи паттерна MVP

Код:

public class Entity
{    public string Title { get; set; }
}
 public class BindingModel
{    private List<Entity> _entities = new List<Entity>() {
        new Entity() {Title = "Entity 1"},
        new Entity() {Title = "Entity 2"},
        new Entity() {Title = "Entity 3"}
    };
     public List<Entity> Entities
    {        get { return _entities; }
    }
}
 public class Presenter
{    private BindingModel _bindingModel = new BindingModel();
     public BindingModel BindingModel
    {        get { return _bindingModel; }
    }
     public void DoAction()
    {        MessageBox.Show("Yep!");
    }
}

Добавили BindingModel (то же самое, что PresentationModel в определении Мартина Фаулера). Поменяем немного XAML:

<UserControl x:Class="SilverlightApplication3.MainPage"     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"   
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     
             xmlns:Controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit" >
     <Grid x:Name="LayoutRoot" Background="White">
        <ListBox HorizontalAlignment="Center" VerticalAlignment="Center" ItemsSource="{Binding Path=Entities}" >
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBox Width="300" Height="100" >
                        <Controls:ContextMenuService.ContextMenu>
                            <Controls:ContextMenu>
                                <Controls:MenuItem Header="Test" Click="MenuItem_Click" />
                            </Controls:ContextMenu>
                        </Controls:ContextMenuService.ContextMenu>
                    </TextBox>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</UserControl>

Ну, и подправим Code Behind:

public partial class MainPage : UserControl
{    Presenter _presenter = new Presenter();
     public MainPage()
    {
        InitializeComponent();
        DataContext = _presenter.BindingModel;
    }
     private void MenuItem_Click(object sender, RoutedEventArgs e)
    {
        _presenter.DoAction();
    }
}

Выводы

А теперь, коллеги, давайте еще раз присмотримся к коду и попробуем ответить на опрос (с подвохом):

Если считаете, что какой-то вариант не рабочий, то прошу рассказать в комментариях почему так. У меня только есть небольшая догадка относительно этого всего, о которой я расскажу чуть позже.

Have feedback or questions? Looking for consultation?

My expertise: MongoDB, ElasticSearch, Splunk, and other databases. Docker, Kubernetes. Logging, Metrics. Performance, memory leaks.

Send me an email to public@denis.gladkikh.email.

The content on this site represents my own personal opinions and thoughts at the time of posting.

Content licensed under the Creative Commons CC BY 4.0.