Привязка к командам в WinForms

Как кнопку можно привязать к команде в модели представления, как в WPF с MVVM?

10 ответов

Решение

Я прикрепил ICommand возражает против Tag собственностью Button а также MenuItem объекты до.

Затем я просто вижу, могу ли я разыграть и запустить его, если смогу, например:

private void button1_Click(object sender, EventArgs e)
{
    ICommand command = ((Control)(sender)).Tag as ICommand;

    if (command != null)
    {
        command.Execute();
    }
}

Чтобы облегчить жизнь, попробуйте разделить элементы управления на подклассы (например, Button, MenuItem)

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

Вот код, который я использую сейчас:

public class CommandManager: Component
{
    private IList<ICommand> Commands { get; set; }
    private IList<ICommandBinder> Binders { get; set; }

    public CommandManager()
    {
        Commands = new List<ICommand>();

        Binders = new List<ICommandBinder>
                      {
                          new ControlBinder(),
                          new MenuItemCommandBinder()
                      };

        Application.Idle += UpdateCommandState;
    }

    private void UpdateCommandState(object sender, EventArgs e)
    {
        Commands.Do(c => c.Enabled);
    }

    public CommandManager Bind(ICommand command, IComponent component)
    {
        if (!Commands.Contains(command))
            Commands.Add(command);

        FindBinder(component).Bind(command, component);
        return this;
    }

    protected ICommandBinder FindBinder(IComponent component)
    {
        var binder = GetBinderFor(component);

        if (binder == null)
            throw new Exception(string.Format("No binding found for component of type {0}", component.GetType().Name));

        return binder;
    }

    private ICommandBinder GetBinderFor(IComponent component)
    {
        var type = component.GetType();
        while (type != null)
        {
            var binder = Binders.FirstOrDefault(x => x.SourceType == type);
            if (binder != null)
                return binder;

            type = type.BaseType;
        }

        return null;
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
            Application.Idle -= UpdateCommandState;

        base.Dispose(disposing);
    }
}

public static class Extensions
{
    public static void Do<T>(this IEnumerable<T> @this, Func<T, object> lambda)
    {
        foreach (var item in @this)
            lambda(item);
    }
}
public abstract class CommandBinder<T> : ICommandBinder where T: IComponent
{
    public Type SourceType
    {
        get { return typeof (T); }
    }

    public void Bind(ICommand command, object source)
    {
        Bind(command, (T) source); 
    }

    protected abstract void Bind(ICommand command, T source);
}

public class ControlBinder: CommandBinder<Control>
{
    protected override void Bind(ICommand command, Control source)
    {
        source.DataBindings.Add("Enabled", command, "Enabled");
        source.DataBindings.Add("Text", command, "Name");
        source.Click += (o, e) => command.Execute();
    }
}

public class MenuItemCommandBinder : CommandBinder<ToolStripItem>
{
    protected override void Bind(ICommand command, ToolStripItem source)
    {
        source.Text = command.Name;
        source.Enabled = command.Enabled;
        source.Click += (o, e) => command.Execute();

        command.PropertyChanged += (o, e) => source.Enabled = command.Enabled;
    }
}

и это пример того, как его использовать:

public partial class Form1 : Form
{
    private CommandManager commandManager;

    public ICommand CommandA { get; set; }
    public ICommand CommandB { get; set; }

    public bool condition;

    public Form1()
    {
        InitializeComponent();

        commandManager = new CommandManager();

        CommandA = new DelegateCommand("Command 1", OnTrue, OnExecute);
        CommandB = new DelegateCommand("Command 2", OnFalse, OnExecute);

        commandManager.Bind(CommandA, button1);
        commandManager.Bind(CommandB, button2);

        commandManager.Bind(CommandA, command1ToolStripMenuItem);
        commandManager.Bind(CommandB, command2ToolStripMenuItem);
    }

    private bool OnFalse()
    {
        return !condition;
    }

    private bool OnTrue()
    {
        return condition;
    }

    private void OnExecute()
    {
        condition = !condition;
    }
}

Также, если вам нужен код, я написал об этом здесь

Вы можете создать общий класс привязки команд, который позволяет привязывать команду к любому классу, который наследуется от ButtonBase,

public class CommandBinding<T> where T : ButtonBase
{
    private T _invoker;
    private ICommand _command;

    public CommandBinding(T invoker, ICommand command)
    {
        _invoker = invoker;
        _command = command;

        _invoker.Enabled = _command.CanExecute(null);
        _invoker.Click += delegate { _command.Execute(null); };
        _command.CanExecuteChanged += delegate { _invoker.Enabled = _command.CanExecute(null); };
    }
}

Привязка команды может быть установлена ​​с помощью следующего кода:

CommandBinding<Button> cmdBinding = 
    new CommandBinding<Button>(btnCut, CutCommand);

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

  • Пример предполагает использование WPF ICommand интерфейс, возможно, придется изменить, если у вас есть собственная реализация шаблона команды.
  • Переданные параметры должны быть проверены на наличие нулевых ссылок.
  • Более конкретная реализация должна иметь некоторый метод удаления обработчиков событий, чтобы избежать утечек памяти.

Общее ограничение также можно изменить на Control который выставляет Click событие и Enabled свойство, которое означает, что команды могут быть связаны практически с любым элементом управления.

Мой подход основан на ответе Гейла, но использует метод расширения и делает привязку одноразовой для управления жизненным циклом:

    public static class ButtonExtensions
    {
        public static IDisposable Bind(this ButtonBase invoker, ICommand command)
        {
            void Click(object sender, EventArgs args) => command.Execute(null);
            void CanExecuteChanged(object sender, EventArgs args) => invoker.Enabled = command.CanExecute(null);

            invoker.Enabled = command.CanExecute(null);

            invoker.Click += Click;
            command.CanExecuteChanged += CanExecuteChanged;

            return Disposable.Create(() =>
            {
                invoker.Enabled = false;
                invoker.Click -= Click;
                command.CanExecuteChanged -= CanExecuteChanged;
            });
        }
    }

Вы можете использовать это так:

private List<IDisposable> Disposables { get; } = new List<IDisposable>();
private ICommand MyCommand { get; }

public MyControl()
{
    MyCommand = DelegateCommand.Create(() => ...);
    Disposables.Add(myButton.Bind(MyCommand));
}

~MyControl()
{
    foreach(var disposable in Disposables)
    {
        disposable?.Dispose();
    }
}

Однако можно предпочесть ReactiveUI, который имеет встроенную поддержку для этого:

ReactiveUI 6.0 и привязка WinForms

button1.Click += (s, e) => new MyCommand().Execute();

Кнопка привязки к ICommand, предложенная другими ответами, была добавлена ​​в .Net7 в качестве функции предварительного просмотра. Теперь вы сможете привязывать кнопки к командам без специальной реализации.

См. документацию: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.buttonbase.command?view=windowsdesktop-7.0 .

Сообщение в блоге MS о привязке команд в winforms:https://devblogs.microsoft.com/dotnet/winforms-cross-platform-dotnet-maui-command-binding/

Если вы хотите привязать команду к элементу управления с помощью конструктора, проверьте это демонстрационное приложение, где я покажу, как использовать MVVM в Windows Forms:

https://bitbucket.org/lbras/mvvmforms

Единственный код, который вы должны написать в выделенном фрагменте кода, - это создание экземпляра модели представления.

Я не думаю, что вы можете сделать это напрямую, но как насчет использования обработчика нажатия кнопки для вызова команды? Это не так чисто, как WPF, но вы все еще получаете свое разделение.

Вам может быть интересен адаптер WAF для Windows Forms. В нем показано, как применить шаблон Model-View-ViewModel (MVVM) в приложении Windows Forms. Реализация адаптера предоставляет решение для отсутствующей поддержки команд в Windows Forms.

Я бы порекомендовал реализовать INotifyPropertyChanged, вы можете использовать его в WinForms, а также в WPF. Смотрите здесь для введения и здесь для некоторой дополнительной информации.

Другие вопросы по тегам