Подтвердить что ты не робот

Прагматичное использование кода в шаблоне MVVM

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

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

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

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

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

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

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

Решение 1) Дайте ViewModel ссылку на представление через абстрактный интерфейс

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

    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, я бы использовал инъекцию зависимости, чтобы создать конкретный вид и ввести его в ViewModel.

Решение 2) Выполнение методов кода с помощью событий

  • 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));
        }
    }
    
  • The View подписывается на эти события

    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 являются хорошими местами для подписки и отмены подписки на события, но я не мог найти проблем во время теста.

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

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

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

Благодарим вас за отзыв!

4b9b3361

Ответ 1

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

И мое личное предпочтение заключается в том, чтобы вернуть ваш вариант один и иметь интерфейс 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
}

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

    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, например Attached Behaviors.

Ответ 2

В частности, для этой проблемы

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

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

ViewModel никогда не должен ни при каких обстоятельствах не знать о представлении, и даже не о IView. в MVVM его "всегда ищут", что означает, что "Вид" может смотреть на виртуальную машину, а виртуальная машина может смотреть на модель. никогда наоборот. Это создает гораздо лучшую ремонтопригодность, так как таким образом ViewModel не выполняет две вещи (пополнение логики и GUI), но только одно. Здесь MVVM превосходит любой предыдущий шаблон MV *.

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

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

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

  • Приложенное поведение.
  • Получение существующих элементов управления и добавление свойств, которые вы хотите.
  • На самом деле писать код в представлении.

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

Подводя итоги

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

Счастливый MVVMing:)

Ответ 3

Я попытаюсь держаться подальше от того, чтобы ViewModel ссылался на представление.

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

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

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

Ответ 4

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

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

Я чувствую, что если вы можете более или менее быть уверены в правильности, просмотрев код кода позади (что в любом случае совпадает с тем, что мы делаем с XAML) и сохраняем основную сложность, где мы можем unit test it - т.е. ViewModel, тогда у нас есть счастливая среда. Слишком просто создать технически чистую MVVM, которая является кошмаром, поддерживающим технику.

Все ИМХО: D

Ответ 5

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

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

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

Ответ 6

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