Прагматичное использование кода в шаблоне MVVM - PullRequest
19 голосов
/ 02 апреля 2011

Я стараюсь следовать шаблону MVVM в приложении WPF настолько хорошо, насколько могу, главным образом, чтобы иметь возможность создавать модульные тесты для моей логики ViewModel.

В большинстве случаев связывание данных между свойствами ViewModel и свойствами визуальных элементов работает нормально и легко. Но иногда я сталкиваюсь с ситуациями, когда не вижу очевидного и прямого пути, в то время как решение для доступа к элементам управления и управления ими из-за кода очень просто.

Вот пример того, что я имею в виду: Вставка фрагмента текста в TextBox в текущей позиции каретки

Поскольку CaretIndex не является свойством зависимости, его нельзя привязать напрямую к свойству ViewModel. Здесь - это решение, позволяющее обойти это ограничение путем создания свойства зависимости. И здесь является решением для этого в коде позади. Я бы предпочел способ с выделенным кодом в этой ситуации. Еще одна проблема, с которой я недавно столкнулся, - это привязка динамической коллекции столбцов к сетке данных WPF. Это было ясно и просто для программирования в коде позади. Но для MVVM-ориентированного подхода к привязке данных я мог найти обходные пути только в нескольких блогах, которые мне показались довольно сложными и имели различные ограничения в том или ином аспекте.

Я не хочу во что бы то ни стало содержать архитектуру MVVM в чистоте от логики выделенного кода. Если количество обходных путей слишком велико, MVVM-дружественное решение требует большого количества кода, который я не до конца понимаю (я все еще новичок в WPF) и занимает слишком много времени, я предпочитаю решение с выделением кода и жертву автоматическая тестируемость нескольких частей моего приложения.

По упомянутым прагматическим причинам я сейчас ищу «шаблоны» для контролируемого использования выделенного кода в приложении, не нарушая архитектуру MVVM или не нарушая его слишком сильно.

До сих пор я нашел и протестировал два решения. Я нарисую грубые наброски на примере Позиции каретки:

Решение 1) Предоставить ViewModel ссылку на View через абстрактный интерфейс

  • У меня был бы интерфейс с методами, которые были бы реализованы представлением:

    public interface IView
    {
        void InsertTextAtCaretPosition(string text);
    }
    
    public partial class View : UserControl, IView
    {
        public View()
        {
            InitializeComponent();
        }
    
        // Interface implementation
        public void InsertTextAtCaretPosition(string text)
        {
            MyTextBox.Text = MyTextBox.Text.Insert(MyTextBox.CaretIndex, text);
        }
    }
    
  • Вставить этот интерфейс в ViewModel

    public class ViewModel : ViewModelBase
    {
        private readonly IView _view;
    
        public ViewModel(IView view)
        {
            _view = view;
        }
    }
    
  • Выполнение кода из обработчика команд ViewModel через методы интерфейса

    public ICommand InsertCommand { get; private set; }
    // Bound for instance to a button command
    
    // Command handler
    private void InsertText(string text)
    {
        _view.InsertTextAtCaretPosition(text);
    }
    

Чтобы создать пару View-ViewModel, я бы использовал внедрение зависимостей для создания экземпляра конкретного View и внедрения его в ViewModel.

Решение 2) Выполнить методы code-behind через события

  • ViewModel является издателем специальных событий, и обработчики команд вызывают эти события

    public class ViewModel : ViewModelBase
    {
        public ViewModel()
        {
        }
    
        public event InsertTextEventHandler InsertTextEvent;
    
        // Command handler
        private void InsertText(string text)
        {
            InsertTextEventHandler handler = InsertTextEvent;
            if (handler != null)
                handler(this, new InsertTextEventArgs(text));
        }
    }
    
  • Представление подписывается на эти события

    public partial class View : UserControl
    {
        public View()
        {
            InitializeComponent();
        }
    
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            ViewModel viewModel = DataContext as ViewModel;
            if (viewModel != null)
                viewModel.InsertTextEvent += OnInsertTextEvent;
        }
    
        private void UserControl_Unloaded(object sender, RoutedEventArgs e)
        {
            ViewModel viewModel = DataContext as ViewModel;
            if (viewModel != null)
                viewModel.InsertTextEvent -= OnInsertTextEvent;
        }
    
        private void OnInsertTextEvent(object sender, InsertTextEventArgs e)
        {
            MyTextBox.Text = MyTextBox.Text.Insert(MyTextBox.CaretIndex, e.Text);
        }
    }
    

Я не уверен, что Loaded и Unloaded события UserControl являются хорошими местами для подписки и отписки на события, но я не смог найти проблем во время теста.

Я проверил оба подхода в двух простых примерах, и оба они, похоже, работают. Теперь мои вопросы:

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

  2. Видите ли вы (и, возможно, практикуете) другие решения?

Спасибо за отзыв заранее!

Ответы [ 6 ]

23 голосов
/ 03 апреля 2011

Специально для этой проблемы

Самое простое решение этого конкретного случая - это добавление присоединенного свойства, которое его выполняет, или поведения.Поведение может быть серебряной пулей для большинства этих случаев, не поддерживаемых rich-gui в mvvm.

Что касается общего случая

ViewModel никогда не следует ни при каких условияхОбстоятельства знают о представлении, и даже не о IView.в MVVM это «всегда смотреть вверх», что означает, что View может смотреть на виртуальную машину, а VM может смотреть на модель.никогда наоборот.Это значительно улучшает удобство обслуживания, так как ViewModel делает не две вещи (заряд логики и графический интерфейс), а только одну вещь.Именно здесь MVVM превосходит любой предыдущий паттерн MV *.

Я бы также попытался воздержаться от использования View в сочетании с ViewModel.это создает уродливый код и разрушаемую зависимость между двумя классами, но иногда, как вы сказали, это более прагматично.Более красивый способ - отправить Свободное сообщение (например, Messenger в MVVMLight или EventAggregator в Prism) из ViewModel в View, и, таким образом, между ними нет сильной зависимости.Некоторые думают, что это лучше, хотя IMO это все еще зависимость.

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

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

  1. Присоединенные поведения.
  2. Извлечение из существующих элементов управления и добавление свойств, которые вы хотите.
  3. На самом деленаписание кода в представлении.

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

Подвести итог

Самая важная вещь, которую вы должны сохранить в MVVM, - это не оставлять код свободным, а хранить всю логику и данные в ViewModel, где представление должно содержать только код, связанный с View.Архитекторы советуют вам вообще не писать код только потому, что это скользкий путь.Вы начинаете писать что-то маленькое, и в итоге вы делаете логические вещи или поддерживаете состояние приложения в представлении, которое является большим нет-нет.

Happy MVVMing:)

7 голосов
/ 03 апреля 2011

Разработка приложений WPF Я нашел оба способа полезными.Если вам нужен всего один вызов из ViewModel в View, второй вариант с обработчиком событий выглядит проще и достаточно хорош.Но если вам требуется более сложный интерфейс между этими уровнями, то имеет смысл ввести интерфейс.

И лично я предпочитаю вернуться к первому варианту и использовать интерфейс IViewAware, реализованный моей ViewModel, и добавить эту ViewModel в View.Выглядит как вариант три.

public interface IViewAware
{
    void ViewActivated();
    void ViewDeactivated();

    event Action CloseView;
}

public class TaskViewModel : ViewModelBase, IViewAware
{

    private void FireCloseRequest()
    {
        var handler = CloseView;
        if (handler != null)
            handler();
    }

    #region Implementation of IViewAware        
    public void ViewActivated()
    {
        // Do something 
    }

    public void ViewDeactivated()
    {
        // Do something 
    }

    public event Action CloseView;    
    #endregion
}

И это упрощенный код для вашего View:

    public View(IViewAware viewModel) : this()
    {
        _viewModel = viewModel;

        DataContext = viewModel;
        Loaded += ViewLoaded;

    }

    void ViewLoaded(object sender, RoutedEventArgs e)
    {
        Activated += (o, v) => _viewModel.ViewActivated();
        Deactivated += (o, v) => _viewModel.ViewDeactivated();

        _viewModel.CloseView += Close;
    }

В реальном приложении я обычно использую внешнюю логику, например, для соединения V и VMПрикрепленное поведение.

2 голосов
/ 03 апреля 2011

Я бы постарался не указывать ViewModel ссылку на View.

Способ сделать это в этом случае:

Извлечь из TextBox и добавить свойство зависимости, котороеоборачивает CaretIndex, подписавшись на событие OnSelectionChanged, которое сообщает, что каретка перемещена.

Таким образом ViewModel может узнать, где находится каретка, связавшись с ней.

1 голос
/ 03 апреля 2011

Зачастую вам нужно работать с элементами управления из кода, когда элемент управления едва ли совместим с MVVM.В этом случае вы можете использовать AttachedProperties, EventTriggers, Behaviors из blend SDK для расширения функциональности элемента управления.Но очень часто я использую наследование , чтобы расширить функциональность управления и сделать его более совместимым с MVVM.Вы можете создать собственный набор элементов управления, унаследованных от базы, с реализованной функциональностью представления.Большим преимуществом этого подхода является то, что вы можете получить доступ к элементам управления ControlTemplate, которые часто необходимы для реализации определенных функций представления.

1 голос
/ 03 апреля 2011

Я бы попытался реализовать это как смешанное поведение текстового поля, подобное этому примеру выбора и расширения древовидного представления без использования кода позади.Я постараюсь привести пример вместе.http://www.codeproject.com/KB/silverlight/ViewModelTree.aspx

Редактировать: Elad уже упоминал об использовании прикрепленных поведений, которые после пары действительно делают такие вещи такими простыми.

Еще один пример поведения для всплывающих окон в mvvmмода: http://www.codeproject.com/KB/silverlight/HisowaSimplePopUpBehavior.aspx

1 голос
/ 02 апреля 2011

На мой взгляд, первый вариант предпочтительнее.Он по-прежнему поддерживает разделение между View и ViewModel (через интерфейс view) и сохраняет вещи в их логических местах.Использование событий менее интуитивно понятно.

Я поддерживаю прагматическое использование кода в ситуациях, когда либо невозможно достичь с помощью привязок, либо требуется добавить сотни строк XAML для достижения того, что мыможно добиться с 3 строками кода позади.

Мне кажется, что если вы можете более или менее быть уверенным в правильности, проверяя код, который находится позади (что в любом случае аналогично тому, что мы делаем с XAML), и сохраняйте основную сложность, где мы можем объединитьпроверить это - то есть ViewModel, то у нас есть счастливое средство.Слишком просто создать технически чистый MVVM, который является кошмаром по ремонту.

Все ИМХО: D

...