Back to Devexpress

MVVM Commands

windowsforms-113965-cross-platform-app-development-winforms-mvvm-concepts-commands.md

latest27.6 KB
Original Source

MVVM Commands

  • Dec 09, 2024
  • 12 minutes to read

In a standard WinForms application, actions are often performed within event handlers. For instance, to refresh data when a user clicks a button, handle the ButtonClick event and retrieve data source records.

This standard technique does not fit the MVVM concept of separated layers. The code that pulls data from a data source should belong to a ViewModel layer, not a View. In MVVM, these tasks are accomplished with commands - ViewModel objects that encapsulate actions. Bind a UI element to this object to achieve the required layer separation: the View code now has only the binding code, while all business logic remains in the ViewModel and can be safely changed.

The DevExpress MVVM Framework treats all public void methods as bindable commands. The code below illustrates how to declare a command that uses a Service to show a message box. You can view the full demo in the DevExpress Demo Center via the following link: Simple Command.

csharp
//POCO ViewModel
public class ViewModelWithSimpleCommand {
    //command
    public void DoSomething() {
        var msgBoxService = this.GetService<IMessageBoxService>();
        msgBoxService.ShowMessage("Hello!");
    }
}
vb
'POCO ViewModel
Public Class ViewModelWithSimpleCommand
    'command
    Public Sub DoSomething()
        Dim msgBoxService = Me.GetService(Of IMessageBoxService)()
        msgBoxService.ShowMessage("Hello!")
    End Sub
End Class

Important

~Command methods raise an exception. Rename these methods or decorate them with the Command attribute. See the following help topic for more information: Conventions and Attributes.

To link a button to this command, use the BindCommand or WithCommand methods.

csharp
//View code
mvvmContext.ViewModelType = typeof(ViewModelWithSimpleCommand);
var fluent = mvvmContext.OfType<ViewModelWithSimpleCommand>();
fluent.BindCommand(commandButton, x => x.DoSomething);
//or
fluent.WithCommand(x => x.DoSomething)
    .Bind(commandButton1);
vb
'View code
mvvmContext.ViewModelType = GetType(ViewModelWithSimpleCommand)
Dim fluent = mvvmContext.OfType(Of ViewModelWithSimpleCommand)()
fluent.BindCommand(commandButton, Sub(x) x.DoSomething)
'or
fluent.WithCommand(Sub(x) x.DoSomething)
    .Bind(commandButton1)

The WithCommand method allows you to bind multiple buttons at the same time.

Run the demo: Bind to Multiple UI Elements.

csharp
//View
var fluent = mvvmContext.OfType<ViewModelWithSimpleCommand>();
fluent.WithCommand(x => x.DoSomething)
    .Bind(commandButton1)
    .Bind(commandButton2);
vb
'View
Dim fluent = mvvmContext.OfType(Of ViewModelWithSimpleCommand)()
fluent.WithCommand(Sub(x) x.DoSomething)
    .Bind(commandButton1)
    .Bind(commandButton2)

CanExecute Conditions

To specify a condition that determines whether a command should or should not run, declare a Boolean method. The method name should start with “Can”, followed by the related command name. These methods are called CanExecute conditions.

csharp
//ViewModel
public class ViewModelWithConditionalCommand {
    //Command
    public void DoSomething() {
        var msgBoxService = this.GetService<IMessageBoxService>();
        msgBoxService.ShowMessage("Hello!");
    }
    //CanExecute condition
    public bool CanDoSomething() {
        return (2 + 2) == 4;
    }
}
vb
'ViewModel
Public Class ViewModelWithConditionalCommand
    'Command
    Public Sub DoSomething()
        Dim msgBoxService = Me.GetService(Of IMessageBoxService)()
        msgBoxService.ShowMessage("Hello!")
    End Sub
    'CanExecute condition
    Public Function CanDoSomething() As Boolean
        Return (2 + 2) = 4
    End Function
End Class

You can also ignore CanExecute name requirements and use the Command attribute to manually assign a command condition.

csharp
[Command(CanExecuteMethodName = "DoSomethingCriteria")]
public void DoSomething(int p) {
    //command
}
vb
<Command(CanExecuteMethodName := "DoSomethingCriteria")>
Public Sub DoSomething(ByVal p As Integer)
    'command
End Sub

If a CanExecute condition returns false , the Framework changes the state of the UI element linked to this command (disables, unchecks, or hides this element). The code sample above is from the following demo: Command with CanExecute condition. Run this demo and change the condition so that it always returns false. The “Execute Command” button becomes disabled because its related command can no longer run.

csharp
//ViewModel
public bool CanDoSomething() {
    //always "false"
    return (2 + 2) == 5;
}
vb
'ViewModel
Public Function CanDoSomething() As Boolean
    'always "False"
    Return (2 + 2) = 5
End Function

The Framework checks the CanExecute condition when:

  • a UI command binding initializes;

  • the RaiseCanExecuteChanged method is called. In the sample below, the return value of the CanDoSomething condition is re-checked every time the SelectedEntity property changes.

  • C#

  • VB.NET

csharp
//Bindable Property
public virtual MyEntity SelectedEntity{ get; set; }

//OnChanged callback for the bindable property
protected void OnSelectedEntityChanged(){
    this.RaiseCanExecuteChanged(x=>x.DoSomething());
}

//Command
public void DoSomething() {
    //. . .
}

//CanExecute condition
public bool CanDoSomething() {
    //. . .
}
vb
'Bindable Property
Public Overridable Property SelectedEntity() As MyEntity

'OnChanged callback for the bindable property
Protected Sub OnSelectedEntityChanged()
    Me.RaiseCanExecuteChanged(Function(x) x.DoSomething())
End Sub

'Command
Public Sub DoSomething()
    '. . .
End Sub

'CanExecute condition
Public Function CanDoSomething() As Boolean
    '. . .
End Function

Commands with Parameters

The DevExpress MVVM Framework accepts public void methods one parameter as parameterized commands. You can use this parameter to pass data between View and ViewModel.

Run the demo: Parameterized Command.

csharp
//ViewModel
public class ViewModelWithParametrizedCommand {
    public void DoSomething(object p) {
        var msgBoxService = this.GetService<IMessageBoxService>();
        msgBoxService.ShowMessage(string.Format("The parameter is {0}.", p));
    }
}

//View
mvvmContext.ViewModelType = typeof(ViewModelWithParametrizedCommand);
var fluent = mvvmContext.OfType<ViewModelWithParametrizedCommand>();
object parameter = 5;
fluent.BindCommand(commandButton, x => x.DoSomething, x => parameter);
vb
'ViewModel
Public Class ViewModelWithParametrizedCommand
    Public Sub DoSomething(ByVal p As Object)
        Dim msgBoxService = Me.GetService(Of IMessageBoxService)()
        msgBoxService.ShowMessage(String.Format("The parameter is {0}.", p))
    End Sub
End Class

'View
mvvmContext.ViewModelType = GetType(ViewModelWithParametrizedCommand)
Dim fluent = mvvmContext.OfType(Of ViewModelWithParametrizedCommand)()
Dim parameter As Object = 5
fluent.BindCommand(commandButton, Sub(x) x.DoSomething(Nothing), Function(x) parameter)

You can also add a parameter to the CanExecute condition.

Run the demo: Parameterized Command with CanExecute Condition.

csharp
//ViewModel
public class ViewModelWithParametrizedConditionalCommand {
    public void DoSomething(int p) {
        var msgBoxService = this.GetService<IMessageBoxService>();
        msgBoxService.ShowMessage(string.Format(
            "The parameter is {0}.", p));
    }
    public bool CanDoSomething(int p) {
        return (2 + 2) == p;
    }
}

//View
mvvmContext.ViewModelType = typeof(ViewModelWithParametrizedConditionalCommand);
var fluent = mvvmContext.OfType<ViewModelWithParametrizedConditionalCommand>();
int parameter = 4;
fluent.BindCommand(commandButton, x => x.DoSomething, x => parameter);
vb
'ViewModel
Public Class ViewModelWithParametrizedConditionalCommand
    Public Sub DoSomething(ByVal p As Integer)
        Dim msgBoxService = Me.GetService(Of IMessageBoxService)()
        msgBoxService.ShowMessage(String.Format("The parameter is {0}.", p))
    End Sub
    Public Function CanDoSomething(ByVal p As Integer) As Boolean
        Return (2 + 2) = p
    End Function
End Class

'View
mvvmContext.ViewModelType = GetType(ViewModelWithParametrizedConditionalCommand)
Dim fluent = mvvmContext.OfType(Of ViewModelWithParametrizedConditionalCommand)()
Dim parameter As Integer = 4
fluent.BindCommand(commandButton, Sub(x) x.DoSomething(Nothing), Function(x) parameter)

Multiple Parameters

Use an object or a tuple data structure to pass multiple parameters. See the following example for an example.

csharp
class Parameters{
    public int Parameter1 { get; set }
    public string Parameter2 { get; set }
    ...
}
// ...
mvvmContext.OfType<MouseDownAwareViewModel>()
        .WithEvent<MouseEventArgs>(label, "MouseDown")
        .EventToCommand(x => x.ReportLocation, args => new Parameters{ Parameter1 = 1, Parameter2 = "2" });
vb
Friend Class Parameters
    Public Property Parameter1() As Integer
        Get
        Set(ByVal value As Integer)
        End Set
        End Get
    public String Parameter2
        Get
        Set(ByVal value As Integer)
        End Set
        End Get
    ...
' ...
mvvmContext.OfType(Of MouseDownAwareViewModel)().WithEvent(Of MouseEventArgs)(label, "MouseDown").EventToCommand(Function(x) x.ReportLocation, Function(args) New Parameters With {.Parameter1 = 1, .Parameter2 = "2"})

Asynchronous Commands

To delay or execute a continuous operation, use asynchronous commands. To create an asynchronous command, declare a public method of the System.Threading.Tasks.Task type (you can use async/await syntax). The code that binds a UI element to the command remains the same. The Framework disables this element while the command is running.

Run the demo: Async Command.

csharp
//ViewModel
public class ViewModelWithAsyncCommand {
    public async Task DoSomethingAsync() {
        // do some work here
        await Task.Delay(1000);
    }
}

//View
mvvmContext.ViewModelType = typeof(ViewModelWithAsyncCommand);
var fluent = mvvmContext.OfType<ViewModelWithAsyncCommand>();
fluent.BindCommand(commandButton, x => x.DoSomethingAsync);
vb
'ViewModel
Public Class ViewModelWithAsyncCommand
    Public Async Sub DoSomethingAsync() As Task
        ' do some work here
        Await Task.Delay(1000)
    End Sub
End Class

'View
mvvmContext.ViewModelType = GetType(ViewModelWithAsyncCommand)
Dim fluent = mvvmContext.OfType(Of ViewModelWithAsyncCommand)()
fluent.BindCommand(commandButton, Sub(x) x.DoSomethingAsync(Nothing))

Tasks support cancellation tokens that allow you to check the IsCancellationRequested property and abort the task when this property returns true. If you add this code to your async command, use the BindCancelCommand method to create a UI element that stops an ongoing async command. The DevExpress MVVM Framework locks this cancel button, and enables it only when a related async command is running.

Run the demo: Async Command with Cancellation.

csharp
//ViewModel
public class ViewModelWithAsyncCommandAndCancellation {
    public async Task DoSomethingAsynchronously() {
        var dispatcher = this.GetService<IDispatcherService>();
        var asyncCommand = this.GetAsyncCommand(x => x.DoSomethingAsynchronously());
        for(int i = 0; i <= 100; i++) {
            if(asyncCommand.IsCancellationRequested)
                break;
            // do some work here
            await Task.Delay(25);
            await UpdateProgressOnUIThread(dispatcher, i);
        }
        await UpdateProgressOnUIThread(dispatcher, 0);
    }

    public int Progress { 
        get; 
        private set; 
    }
    //update the "Progress" property bound to the progress bar within a View
    async Task UpdateProgressOnUIThread(IDispatcherService dispatcher, int progress) {
        await dispatcher.BeginInvoke(() => {
            Progress = progress;
            this.RaisePropertyChanged(x => x.Progress);
        });
    }
}

//View
mvvmContext.ViewModelType = typeof(ViewModelWithAsyncCommandAndCancellation);
var fluent = mvvmContext.OfType<ViewModelWithAsyncCommandAndCancellation>();
fluent.BindCommand(commandButton, x => x.DoSomethingAsynchronously);
fluent.BindCancelCommand(cancelButton, x => x.DoSomethingAsynchronously);
fluent.SetBinding(progressBar, p => p.EditValue, x => x.Progress);
vb
'ViewModel
Public Class ViewModelWithAsyncCommandAndCancellation
    Public Async Sub DoSomethingAsynchronously() As Task
        Dim dispatcher = Me.GetService(Of IDispatcherService)()
        Dim asyncCommand = Me.GetAsyncCommand(Sub(x) x.DoSomethingAsynchronously())
        For i As Integer = 0 To 100
            If asyncCommand.IsCancellationRequested Then
                Exit For
            End If
            ' do some work here
            Await Task.Delay(25)
            Await UpdateProgressOnUIThread(dispatcher, i)
        Next i
        Await UpdateProgressOnUIThread(dispatcher, 0)
    End Sub

    Private privateProgress As Integer
    Public Property Progress() As Integer
        Get
            Return privateProgress
        End Get
        Private Set(ByVal value As Integer)
            privateProgress = value
        End Set
    End Property
    'update the "Progress" property bound to the progress bar within a View
    Private Async Sub UpdateProgressOnUIThread(ByVal dispatcher As IDispatcherService, ByVal progress As Integer) As Task
        Await dispatcher.BeginInvoke(Sub()
            Me.Progress = progress
            Me.RaisePropertyChanged(Sub(x) x.Progress)
        End Sub)
    End Sub
End Class

'View
mvvmContext.ViewModelType = GetType(ViewModelWithAsyncCommandAndCancellation)
Dim fluent = mvvmContext.OfType(Of ViewModelWithAsyncCommandAndCancellation)()
fluent.BindCommand(commandButton, Sub(x) x.DoSomethingAsynchronously)
fluent.BindCancelCommand(cancelButton, Sub(x) x.DoSomethingAsynchronously)
fluent.SetBinding(progressBar, Sub(p) p.EditValue, Sub(x) x.Progress)

The WithCommand Fluent API method also supports cancelable asynchronous commands.

csharp
mvvmContext.ViewModelType = typeof(ViewModelWithAsyncCommandAndCancellation);
// Initialize the Fluent API
var fluent = mvvmContext.OfType<ViewModelWithAsyncCommandAndCancellation>();
// Binding for buttons
fluent.WithCommand(x => x.DoSomethingAsynchronously)
    .Bind(commandButton)
    .BindCancel(cancelButton);
vb
mvvmContext.ViewModelType = GetType(ViewModelWithAsyncCommandAndCancellation)
' Initialize the Fluent API
Dim fluent = mvvmContext.OfType(Of ViewModelWithAsyncCommandAndCancellation)()
' Binding for buttons
fluent.WithCommand(Sub(x) x.DoSomethingAsynchronously).Bind(commandButton).BindCancel(cancelButton)

Command Triggers

Triggers allow you to perform additional View actions associated with a command. Trigger types depend on the condition that sets off the trigger. These include:

  • “Before” trigger - allows you to perform actions before a target command executes.
csharp
mvvmContext.ViewModelType = typeof(ViewModelWithSimpleCommand);
var fluent = mvvmContext.OfType<ViewModelWithSimpleCommand>();
fluent.BindCommand(commandButton, x => x.DoSomething);
fluent.WithCommand(x => x.DoSomething)
.Before(() => XtraMessageBox.Show("The target command is about to be executed"));
vb
mvvmContext.ViewModelType = GetType(ViewModelWithSimpleCommand)
Dim fluent = mvvmContext.OfType(Of ViewModelWithSimpleCommand)()
fluent.BindCommand(commandButton, Function(x) x.DoSomething)
fluent.WithCommand(Sub(x) x.DoSomething)
.Before(Function() XtraMessageBox.Show("The target command is about to be executed"))
  • “After” trigger - allows you to perform actions after a target command finishes.
csharp
mvvmContext.ViewModelType = typeof(ViewModelWithSimpleCommand);
var fluent = mvvmContext.OfType<ViewModelWithSimpleCommand>();
fluent.BindCommand(commandButton, x => x.DoSomething);
fluent.WithCommand(x => x.DoSomething)
.After(() => XtraMessageBox.Show("The target command has been executed"));
vb
mvvmContext.ViewModelType = GetType(ViewModelWithSimpleCommand)
Dim fluent = mvvmContext.OfType(Of ViewModelWithSimpleCommand)()
fluent.BindCommand(commandButton, Function(x) x.DoSomething)
fluent.WithCommand(Function(x) x.DoSomething).After(Function() XtraMessageBox.Show("The target command has been executed"))
  • “CanExecute” condition trigger - allows you to perform actions when a CanExecute condition for the target command changes.
csharp
var fluent = mvvmContext.OfType<ViewModelWithSimpleCommandAndCanExecute>();
fluent.BindCommand(commandButton, x => x.DoSomething);
// When the CanExecute condition changes, the message shows up
fluent.WithCommand(x => x.DoSomething)
.OnCanExecuteChanged(() => XtraMessageBox.Show("The CanExecute condition has changed"));
vb
Dim fluent = mvvmContext.OfType(Of ViewModelWithSimpleCommandAndCanExecute)()
fluent.BindCommand(commandButton, Function(x) x.DoSomething)
' When the CanExecute condition changes, the message shows up
fluent.WithCommand(Function(x) x.DoSomething)
.OnCanExecuteChanged(Function() XtraMessageBox.Show("The CanExecute condition has changed"))

Triggers are executed for every UI element bound to the target command. The following code snippet displays a message box when any of the buttons is clicked:

csharp
mvvmContext1.OfType<BulkEditViewModel>()
    .WithCommand(vm => vm.RemoveFields())
    .Bind(button1)
    .Bind(button2)
    .After(() => MessageBox.Show("Test"));
vb
mvvmContext1.OfType(Of BulkEditViewModel)()
    .WithCommand(Function(vm) vm.RemoveFields())
    .Bind(button1)
    .Bind(button2)
    .After(Function() MessageBox.Show("Test"))

Non-POCO Commands

The POCO class commands described above allow you to use the most straightforward and fail-free syntax. The DevExpress MVVM Framework also supports other command types - to ensure hassle-free migration for legacy projects.

DevExpress DelegateCommand Objects

Delegate commands are an implementation of the System.Windows.Input.ICommand interface.

Run the demo: Simple Commands

csharp
DelegateCommand command = new DelegateCommand(() => {
    XtraMessageBox.Show("Hello!");
});
commandButton.BindCommand(command);
vb
Dim command As New DelegateCommand(Sub() XtraMessageBox.Show("Hello!"))
commandButton.BindCommand(command)

Run the demo: Commands with CanExecute Conditions

csharp
Func<bool> canExecute = () => (2 + 2 == 4);
DelegateCommand command = new DelegateCommand(() => {
    XtraMessageBox.Show("Hello!");
}, canExecute);
commandButton.BindCommand(command);
vb
Dim canExecute As Func(Of Boolean) = Function() (2 + 2 = 4)
Dim command As New DelegateCommand(Sub() XtraMessageBox.Show("Hello!"), canExecute)
commandButton.BindCommand(command)

Run the demo: Commands with Parameters

csharp
DelegateCommand<object> command = new DelegateCommand<object>((v) => {
    XtraMessageBox.Show(string.Format("The parameter is {0}.", v));
});
object parameter = 5;
commandButton.BindCommand(command, () => parameter);
vb
Dim command As New DelegateCommand(Of Object)(Sub(v) XtraMessageBox.Show(String.Format("The parameter is {0}.", v)))
Dim parameter As Object = 5
commandButton.BindCommand(command, Function() parameter)

Run the demo: Commands with Parameterized CanExecute Conditions

csharp
Func<int, bool> canExecute = (p) => (2 + 2 == p);
DelegateCommand<int> command = new DelegateCommand<int>((v) => {
    XtraMessageBox.Show(string.Format("The parameter is {0}.", v));
}, canExecute);
int parameter = 4;
commandButton.BindCommand(command, () => parameter);
vb
Dim canExecute As Func(Of Integer, Boolean) = Function(p) (2 + 2 = p)
Dim command As New DelegateCommand(Of Integer)(Sub(v) XtraMessageBox.Show(String.Format("The parameter is {0}.", v)), canExecute)
Dim parameter As Integer = 4
commandButton.BindCommand(command, Function() parameter)

Custom Command Classes

These are objects of any custom type that have at least one Execute method. If needed, you can add the CanExecute method and CanExecuteChanged event.

Run the demo: Simple Commands

csharp
CommandObject command = new CommandObject();
commandButton.BindCommand(command);

public class CommandObject {
    public void Execute(object parameter) {
        XtraMessageBox.Show("Hello!");
    }
}
vb
Private command As New CommandObject()
commandButton.BindCommand(command)

Public Class CommandObject
    Public Sub Execute(ByVal parameter As Object)
        XtraMessageBox.Show("Hello!")
    End Sub
End Class

Run the demo: Commands with Parameters

csharp
CommandObjectWithParameter command = new CommandObjectWithParameter();
int parameter = 4;
commandButton.BindCommand(command, () => parameter);

public class CommandObjectWithParameter {
    public void Execute(object parameter) {
        XtraMessageBox.Show(string.Format(
            "The parameter is {0}.", parameter));
    }
    public bool CanExecute(object parameter) {
        return object.Equals(2 + 2, parameter);
    }
}
vb
Dim command As New CommandObjectWithParameter()
Dim parameter As Integer = 4
commandButton.BindCommand(command, Sub() parameter)

Public Class CommandObjectWithParameter
    Public Sub Execute(ByVal parameter As Object)
        XtraMessageBox.Show(String.Format("The parameter is {0}.", parameter))
    End Sub
    Public Function CanExecute(ByVal parameter As Object) As Boolean
        Return Object.Equals(2 + 2, parameter)
    End Function
End Class

Command Binding for Calling Methods inside the UI

If you have a command defined in your ViewModel, use the following command-related properties and events of button-based UI elements for binding:

  • Command
  • CommandParameter
  • CommandCanExecuteChanged
  • CommandChanged

These APIs are available in the following classes: BaseButton, ContextButtonBase, DXMenuItem, JumpListItemTask, AdornerElement, BarButtonItem, BarCheckItem, BarToggleSwitchItem, WindowsUIButton, AccordionControlElement, NavButton, NavigationBarItem, TileNavElement, BackstageViewButtonItem, GalleryItem, RecentItemBase, NavigatorButtonBase, TileItem, EditorButton, NavBarItem.

Example

csharp
// Creates a simple command that displays a message.
DelegateCommand command = new DelegateCommand(() => {
    XtraMessageBox.Show("Hello World!");
});
// Binds the Command property of a button.
commandButton.Command = command;
vb
' This is a simple command that displays a message.
Dim command As DelegateCommand = New DelegateCommand(Sub()
    XtraMessageBox.Show("Hello! I'm running!")
End Sub)
' Binds the Command property of a button.
commandButton.Command = command

Run Demo: Simple Command Run Demo: Parameterized Command

Read the following topics for additional information: