Реализация MVVM в WPF без использования System.Windows.Input.ICommand - PullRequest
8 голосов
/ 02 февраля 2009

Я пытаюсь реализовать приложение WPF, используя шаблон MVVM (Model-View-ViewModel), и я хотел бы, чтобы деталь View была в отдельной сборке (EXE) из частей Model и ViewModel (DLL) .

Суть в том, чтобы убрать сборку Model / ViewModel от любой зависимости WPF. Причиной этого является то, что я хотел бы повторно использовать его из исполняемых файлов с другими (не WPF) технологиями пользовательского интерфейса, например WinForms или GTK # под Mono.

По умолчанию это не может быть сделано, потому что ViewModel предоставляет одну или несколько ICommands. Но тип ICommand определен в пространстве имен System.Windows.Input, которое принадлежит WPF!

Итак, есть ли способ удовлетворить механизм привязки WPF без использования ICommand?

Спасибо!

Ответы [ 8 ]

7 голосов
/ 02 февраля 2009

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

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

Простейшим примером будет оболочка для делегата void с методом Execute.

Все разные слои графического интерфейса просто необходимо преобразовать из их собственных типов команд в ваши пользовательские типы команд в одном месте.

5 голосов
/ 03 февраля 2009

WinForms не имеет развитой инфраструктуры привязки данных и команд, необходимой для использования модели представления в стиле MVVM.

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

Я не использовал GTK # в реальном проекте, поэтому я понятия не имею, что он может или не может делать, но я подозреваю, что MVVM в любом случае не является оптимальным подходом для GTK #.

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

Затем для WinForms просто удалите модель представления и напрямую вызовите модель из пользовательского интерфейса или создайте другой промежуточный слой, основанный на более ограниченной поддержке связывания данных WinForms.

Повторите для GTK # или напишите MVC-контроллеры и представления, чтобы дать модели веб-интерфейс.

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

4 голосов
/ 10 февраля 2011

Мне нужен пример этого, поэтому я написал один, используя различные методы.

Я имел в виду несколько целей дизайна

1 - будь проще

2 - абсолютно нет кода в представлении (класс окна)

3 - демонстрировать зависимость только от ссылки System в библиотеке классов ViewModel.

4 - сохранить бизнес-логику во ViewModel и направить ее непосредственно к соответствующим методам, не написав кучу методов-заглушек.

Вот код ...

App.xaml (нет StartupUri - единственное, на что стоит обратить внимание)

<Application 
    x:Class="WpfApplicationCleanSeparation.App" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
</Application>

App.xaml.cs (загрузить главный вид)

using System.Windows;
using WpfApplicationCleanSeparation.ViewModels;

namespace WpfApplicationCleanSeparation
{
    public partial class App
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            var view = new MainView();
            var viewModel = new MainViewModel();

            view.InitializeComponent();
            view.DataContext = viewModel;
            CommandRouter.WireMainView(view, viewModel);
            view.Show();
        }
    }
}

CommandRouter.cs (магия)

using System.Windows.Input;
using WpfApplicationCleanSeparation.ViewModels;

namespace WpfApplicationCleanSeparation
{
    public static class CommandRouter
    {
        static CommandRouter()
        {
            IncrementCounter = new RoutedCommand();
            DecrementCounter = new RoutedCommand();
        }

        public static RoutedCommand IncrementCounter { get; private set; }
        public static RoutedCommand DecrementCounter { get; private set; }

        public static void WireMainView(MainView view, MainViewModel viewModel)
        {
            if (view == null || viewModel == null) return;

            view.CommandBindings.Add(
                new CommandBinding(
                    IncrementCounter,
                    (λ1, λ2) => viewModel.IncrementCounter(),
                    (λ1, λ2) =>
                        {
                            λ2.CanExecute = true;
                            λ2.Handled = true;
                        }));
            view.CommandBindings.Add(
                new CommandBinding(
                    DecrementCounter,
                    (λ1, λ2) => viewModel.DecrementCounter(),
                    (λ1, λ2) =>
                        {
                            λ2.CanExecute = true;
                            λ2.Handled = true;
                        }));
        }
    }
}

MainView.xaml (нет кода, буквально удален!)

<Window 
    x:Class="WpfApplicationCleanSeparation.MainView" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:WpfApplicationCleanSeparation="clr-namespace:WpfApplicationCleanSeparation" 
    Title="MainWindow" 
    Height="100" 
    Width="100">
    <StackPanel>
        <TextBlock Text="{Binding Counter}"></TextBlock>
        <Button Content="Decrement" Command="WpfApplicationCleanSeparation:CommandRouter.DecrementCounter"></Button>
        <Button Content="Increment" Command="WpfApplicationCleanSeparation:CommandRouter.IncrementCounter"></Button>
    </StackPanel>
</Window>

MainViewModel.cs (включает в себя и саму модель, поскольку этот пример настолько упрощен, пожалуйста, извините за срыв шаблона MVVM.

using System.ComponentModel;

namespace WpfApplicationCleanSeparation.ViewModels
{
    public class CounterModel
    {
        public int Data { get; private set; }

        public void IncrementCounter()
        {
            Data++;
        }

        public void DecrementCounter()
        {
            Data--;
        }
    }

    public class MainViewModel : INotifyPropertyChanged
    {
        private CounterModel Model { get; set; }
        public event PropertyChangedEventHandler PropertyChanged = delegate { };

        public MainViewModel()
        {
            Model = new CounterModel();
        }

        public int Counter
        {
            get { return Model.Data; }
        }

        public void IncrementCounter()
        {
            Model.IncrementCounter();

            PropertyChanged(this, new PropertyChangedEventArgs("Counter"));
        }

        public void DecrementCounter()
        {
            Model.DecrementCounter();

            PropertyChanged(this, new PropertyChangedEventArgs("Counter"));
        }
    }
}

Proof

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

Happy Coding:)

РЕДАКТИРОВАТЬ: Чтобы упростить мой собственный код, вы можете найти это полезным для превращения надстроек в однострочники.

    private static void Wire(this UIElement element, RoutedCommand command, Action action)
    {
        element.CommandBindings.Add(new CommandBinding(command, (sender, e) => action(), (sender, e) => { e.CanExecute = true; }));
    }
3 голосов
/ 05 ноября 2013

Извините, Дейв, но мне не очень понравилось ваше решение. Сначала вы должны вручную закодировать сантехнику для каждой команды в коде, затем вам нужно настроить CommandRouter, чтобы он знал о каждой ассоциации представления / представления модели в приложении.

Я выбрал другой подход.

У меня есть сборка утилиты Mvvm (у которой нет зависимостей WPF), которую я использую в своей модели представления. В этой сборке я объявляю пользовательский интерфейс ICommand и класс DelegateCommand, который реализует этот интерфейс.

namespace CommonUtil.Mvvm
{
    using System;


    public interface ICommand
    {
        void Execute(object parameter);
        bool CanExecute(object parameter);

        event EventHandler CanExecuteChanged;
    }

    public class DelegateCommand : ICommand
    {
        public DelegateCommand(Action<object> execute) : this(execute, null)
        {

        }

        public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
        {
            _execute = execute;
            _canExecute = canExecute;
        }

        public void Execute(object parameter)
        {
            _execute(parameter);
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null || _canExecute(parameter);
        }


        public event EventHandler CanExecuteChanged;

        private readonly Action<object> _execute;
        private readonly Func<object, bool> _canExecute;
    }
}

У меня также есть сборка библиотеки Wpf (которая ссылается на системные библиотеки WPF), на которую я ссылаюсь из своего проекта пользовательского интерфейса WPF. В этой сборке я объявляю класс CommandWrapper, который имеет стандартный интерфейс System.Windows.Input.ICommand. CommandWrapper создается с использованием экземпляра моей пользовательской ICommand и просто делегирует Execute, CanExecute и CanExecuteChanged непосредственно в мой пользовательский тип ICommand.

namespace WpfUtil
{
    using System;
    using System.Windows.Input;


    public class CommandWrapper : ICommand
    {
        // Public.

        public CommandWrapper(CommonUtil.Mvvm.ICommand source)
        {
            _source = source;
            _source.CanExecuteChanged += OnSource_CanExecuteChanged;
            CommandManager.RequerySuggested += OnCommandManager_RequerySuggested;
        }

        public void Execute(object parameter)
        {
            _source.Execute(parameter);
        }

        public bool CanExecute(object parameter)
        {
            return _source.CanExecute(parameter);
        }

        public event System.EventHandler CanExecuteChanged = delegate { };


        // Implementation.

        private void OnSource_CanExecuteChanged(object sender, EventArgs args)
        {
            CanExecuteChanged(sender, args);
        }

        private void OnCommandManager_RequerySuggested(object sender, EventArgs args)
        {
            CanExecuteChanged(sender, args);
        }

        private readonly CommonUtil.Mvvm.ICommand _source;
    }
}

В моей сборке Wpf я также создаю ValueConverter, который при передаче экземпляра моей пользовательской ICommand выделяет экземпляр совместимого с Windows.Input.ICommand CommandWrapper.

namespace WpfUtil
{
    using System;
    using System.Globalization;
    using System.Windows.Data;


    public class CommandConverter : IValueConverter
    {

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return new CommandWrapper((CommonUtil.Mvvm.ICommand)value);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new System.NotImplementedException();
        }
    }
}

Теперь мои viewmodels могут выставлять команды как экземпляры моего пользовательского типа команд без необходимости иметь какую-либо зависимость от WPF, и мой пользовательский интерфейс может связывать команды Windows.Input.ICommand с этими viewmodels, используя мой ValueConverter следующим образом. (Спам в пространстве имен XAML опущен).

<Window x:Class="Project1.MainWindow">

    <Window.Resources>
        <wpf:CommandConverter x:Key="_commandConv"/>
    </Window.Resources>

    <Grid>
        <Button Content="Button1" Command="{Binding CustomCommandOnViewModel,
                                        Converter={StaticResource _commandConv}}"/>
    </Grid>

</Window>

Теперь, если я действительно ленив (что я и есть), и меня не беспокоит необходимость вручную применять CommandConverter каждый раз, тогда в моей сборке Wpf я могу создать свой собственный подкласс Binding, например:

namespace WpfUtil
{
    using System.Windows.Data;


    public class CommandBindingExtension : Binding
    {
        public CommandBindingExtension(string path) : base(path)
        {
            Converter = new CommandConverter();
        }
    }
}

Так что теперь я могу связать свой собственный тип команды еще проще, например:

<Window x:Class="Project1.MainWindow"
                xmlns:wpf="clr-namespace:WpfUtil;assembly=WpfUtil">

    <Window.Resources>
        <wpf:CommandConverter x:Key="_commandConv"/>
    </Window.Resources>

    <Grid>
        <Button Content="Button1" Command="{wpf:CommandBinding CustomCommandOnViewModel}"/>
    </Grid>

</Window>
2 голосов
/ 22 января 2010

Конечно, это возможно. Вы можете создать еще один уровень абстракции. Добавьте свой собственный интерфейс IMyCommand, аналогичный или аналогичный интерфейсу ICommand, и используйте его.

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

См. Полное решение Silverlight 4 здесь Модальные диалоги с MVVM и Silverlight 4

2 голосов
/ 28 марта 2009

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

1 голос
/ 06 мая 2011

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

VM - это адаптация модели для соответствия WPF Views. Я хотел бы сохранить ВМ простой и сделать это.

Я не могу представить, как заставить MVVM работать с Winforms. OTOH, имея только логику модели и бизнеса, вы можете ввести их непосредственно в форму, если это необходимо.

0 голосов
/ 25 июля 2011

«Вы не можете повторно использовать WPF MVVM в приложении WinForms»

Для этого см. URL http://waf.codeplex.com/, я использовал MVVM в Win Form, теперь, когда бы я ни захотел обновить презентацию приложения с Win Form до WPF, она будет изменена без изменения логики приложения,

Но у меня есть одна проблема с повторным использованием ViewModel в Asp.net MVC, поэтому я могу сделать то же самое приложение для рабочего стола в Интернете без каких-либо изменений в логике приложения.

Спасибо ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...