Back to Devexpress

Navigation and View Management

windowsforms-114173-cross-platform-app-development-winforms-mvvm-concepts-view-management.md

latest22.3 KB
Original Source

Navigation and View Management

  • Dec 09, 2024
  • 12 minutes to read

This topic explains how to implement navigation between separate application Views, and build the View-ViewModel relations.

Standard Navigation Services

DevExpress MVVM Framework includes a number of Services that you can utilize to implement navigation between different application modules (Views).

The use of any MVVM Service consists of three major steps:

  1. Register a Service in a View. The Service can be registered either globally (it will be available from any application View) or locally (if you intend to use it from this module only):

  2. Declare a property inside a ViewModel to retrieve an instance of a registered Service.

  3. Call a public API of the Service instance inside your ViewModel.

For example, the main application View has the MvvmContext component that links this main application form (View) to the “Form1ViewModel” ViewModel.

csharp
// View
mvvmContext1.ViewModelType = typeof(mvvmNavi.Form1ViewModel);

// ViewModel
[POCOViewModel()]
public class Form1ViewModel {
    //...
}
vb
' View
mvvmContext1.ViewModelType = GetType(mvvmNavi.Form1ViewModel)

    Private Sub InitializeBindings()
        Dim fluent = mvvmContext1.OfType(Of Form1ViewModel)()
    End Sub
End Class

' ViewModel
<POCOViewModel()>
Public Class Form1ViewModel
    '...
End Class

The application also has two UserControls, each with its own MvvmContext component. A UserControl’s View is linked to its corresponding ViewModel.

csharp
public partial class ViewA : UserControl {
    MVVMContext mvvmContext;
    public ViewA() {
        mvvmContext = new MVVMContext();
        mvvmContext.ContainerControl = this;
        mvvmContext.ViewModelType = typeof(ViewAViewModel);
    }
}

public class ViewAViewModel {
}

public partial class ViewB : UserControl {
    MVVMContext mvvmContext;
    public ViewB() {
        mvvmContext = new MVVMContext();
        mvvmContext.ContainerControl = this;
        mvvmContext.ViewModelType = typeof(ViewBViewModel);
    }
}

public class ViewBViewModel {
}
vb
Partial Public Class ViewA
    Inherits UserControl

    Private mvvmContext As MVVMContext
    Public Sub New()
        mvvmContext = New MVVMContext()
        mvvmContext.ContainerControl = Me
        mvvmContext.ViewModelType = GetType(ViewAViewModel)
    End Sub
End Class

Public Class ViewAViewModel
End Class

Partial Public Class ViewB
    Inherits UserControl

    Private mvvmContext As MVVMContext
    Public Sub New()
        mvvmContext = New MVVMContext()
        mvvmContext.ContainerControl = Me
        mvvmContext.ViewModelType = GetType(ViewBViewModel)
    End Sub
End Class

Public Class ViewBViewModel
End Class

Note

The code above initializes MvvmContext components and sets their ViewModelType properties for illustrative purposes only. In real-life applications, it is recommended to place components onto Forms and UserControls at design time, and use smart tag menus to set up ViewModels.

The following examples illustrate how to choose and utilize different DevExpress Services depending on your task:

Example 1: DocumentManager Tabs

The main application form (View) has an empty Document Manager, and the task is to display UserControls A and B as DocumentManager tabs (Documents).

To manage DocumentManager documents, use the DocumentManagerService. Register it inside the main View:

csharp
public Form1() {
    InitializeComponent();
    //. . .
    var service = DocumentManagerService.Create(tabbedView1);
    service.UseDeferredLoading = DevExpress.Utils.DefaultBoolean.True;
    mvvmContext1.RegisterDefaultService(service);
}
vb
Public Sub Form1()
    InitializeComponent()
    '. . .
    Dim service = DocumentManagerService.Create(tabbedView1)
    service.UseDeferredLoading = DevExpress.Utils.DefaultBoolean.True
    mvvmContext1.RegisterDefaultService(service)
End Sub

In the main ViewModel, implement a property that retrieves an instance of the registered Service:

csharp
[POCOViewModel()]
public class Form1ViewModel {
    protected IDocumentManagerService DocumentManagerService {
        get { return this.GetService<IDocumentManagerService>(); }
    }
}
vb
<POCOViewModel()>
Public Class Form1ViewModel
    Protected ReadOnly Property DocumentManagerService() As IDocumentManagerService
        Get
            Return Me.GetService(Of IDocumentManagerService)()
        End Get
    End Property
End Class

The DocumentManagerService.CreateDocument and DocumentManagerService.FindDocumentById methods allow you to create and locate Documents. Then you can call the IDocument.Show method to display them.

csharp
// main ViewModel
public void CreateDocument(object id, string documentType, string title) {
    var document = DocumentManagerService.FindDocumentById(id);
    if (document == null) {
        document = DocumentManagerService.CreateDocument(
            documentType, parameter: null, parentViewModel: this);
        document.Id = id;
        document.Title = title;
    }
    document.Show();
}
vb
' main ViewModel
Public Sub CreateDocument(ByVal id As Object, ByVal documentType As String, ByVal title As String)
    Dim document = DocumentManagerService.FindDocumentById(id)
    If document Is Nothing Then
        document = DocumentManagerService.CreateDocument(documentType, parameter:= Nothing, parentViewModel:=Me)
        document.Id = id
        document.Title = title
    End If
    document.Show()
End Sub

This core method can be used in various scenarios.

  • Create a new Document with the specific UserControl and load it on application startup:

  • Create one Document for each UserControl and load all of these Documents at startup.

  • Bind UI elements (for instance, Ribbon buttons) to a command that creates a new Document with the specific UserControl.

Run Demo: Open the specific module Run Demo: Open all modules Run Demo: Open or activate the specific contact

Example 2: Navigation Frame

The main form (View) has an empty NavigationFrame component. This component can store multiple pages, but allows users to view only one page at a time. To populate this component with pages and implement navigation, use the NavigationService.

Global Service registration:

csharp
// main View
var service = NavigationService.Create(navigationFrame1);
mvvmContext1.RegisterDefaultService(service);
vb
' main View
Dim service = NavigationService.Create(navigationFrame1)
mvvmContext1.RegisterDefaultService(service)

The property that retrieves a Service instance:

csharp
// main ViewModel
protected INavigationService NavigationService {
    get { return this.GetService<INavigationService>(); }
}
vb
' main ViewModel
Protected ReadOnly Property NavigationService() As INavigationService
    Get
        Return Me.GetService(Of INavigationService)()
    End Get
End Property

Navigation:

csharp
// main View
var fluent = mvvmContext.OfType<RootViewModel>();
fluent.WithEvent(mainView, "Load")
    .EventToCommand(x => x.OnLoad);

// main ViewModel

public void OnLoad() {
    NavigationService.Navigate("ViewA", null, this);
}
vb
' main View
Private fluent = mvvmContext.OfType(Of RootViewModel)()
fluent.WithEvent(mainView, "Load").EventToCommand(Function(x) x.OnLoad)

' main ViewModel

public void OnLoad()
    NavigationService.Navigate("ViewA", Nothing, Me)

The Navigate method can accept parameters as its second argument. This allows you to pass any data between navigated modules. The DevExpress Demo Center sample illustrates how to pass the name of a previosly active module to the currently selected View. Note in this example the global Service registration allows every child ViewModel to utilize this Service’s API.

Run Demo: Open the specific module and close the previous

Example 3: Modal Forms

In this example, child Views are shown as separate forms above other application windows. To do this, use the WindowedDocumentManagerService Service.

Local registration:

csharp
// main View
var service = WindowedDocumentManagerService.Create(mainView);
service.DocumentShowMode = WindowedDocumentManagerService.FormShowMode.Dialog;
mvvmContext.RegisterService(service);
vb
' main View
Dim service = WindowedDocumentManagerService.Create(mainView)
service.DocumentShowMode = WindowedDocumentManagerService.FormShowMode.Dialog
mvvmContext.RegisterService(service)

The property that retrieves a Service instance:

csharp
// main ViewModel
protected IDocumentManagerService WindowedDocumentManagerService {
    get { return this.GetService<IDocumentManagerService>(); }
}
vb
' main ViewModel
Protected ReadOnly Property WindowedDocumentManagerService() As IDocumentManagerService
    Get
        Return Me.GetService(Of IDocumentManagerService)()
    End Get
End Property

Navigation:

csharp
// main View
var fluent = mvvmContext.OfType<MainViewModel>();
fluent.BindCommand(showBtn, x => x.ShowAcceptDialog);

// main ViewModel
int id = 0;
public void ShowAcceptDialog() {
    var viewModel = ViewModelSource.Create(() => new ViewAViewModel());
    var document = WindowedDocumentManagerService.FindDocumentById(id);
    if(document == null) {
        document = WindowedDocumentManagerService.CreateDocument(string.Empty, viewModel: viewModel);
        document.Id = id;
        document.Title = "Accept Dialog";
    }
    document.Show();
}
vb
' main View
Dim fluent = mvvmContext.OfType(Of MainViewModel)()
fluent.BindCommand(showBtn, Function(x) x.ShowAcceptDialog)

' main ViewModel
Private id As Integer = 0
Public Sub ShowAcceptDialog()
    Dim viewModel = ViewModelSource.Create(Function() New ViewAViewModel())
    Dim document = WindowedDocumentManagerService.FindDocumentById(id)
    If document Is Nothing Then
        document = WindowedDocumentManagerService.CreateDocument(String.Empty, viewModel:= viewModel)
        document.Id = id
        document.Title = "Accept Dialog"
    End If
    document.Show()
End Sub

Run Demo: Open the specific modal form

Close a modal form:

csharp
public class ChildViewModel : IDocumentContent {
    public void Close() {
        // Closes the document.
        DocumentOwner?.Close(this);
    }
    public IDocumentOwner DocumentOwner { get; set; }
    public object Title { get; set; }
    void IDocumentContent.OnClose(CancelEventArgs e) {
        /* Do something */
    }
    void IDocumentContent.OnDestroy() {
        /* Do something */
    }
}
vb
Public Class ChildViewModel
    Implements IDocumentContent

    Public Sub Close()
        ' Closes the document.
        DocumentOwner?.Close(Me)
    End Sub
    Public Property DocumentOwner() As IDocumentOwner
    Public Property Title() As Object
    Private Sub IDocumentContent_OnClose(ByVal e As CancelEventArgs) Implements IDocumentContent.OnClose
        ' Do something 
    End Sub
    Private Sub IDocumentContent_OnDestroy() Implements IDocumentContent.OnDestroy
        ' Do something 
    End Sub
End Class

Run Demo: Open and Close a Modal Form

ViewType Attribute

If you follow naming conventions (a ViewModel for the “ModuleX” View is called “ModuleXViewModel”) and Views/ViewModels are located in the same namespace, the default use of MVVM Services shown in the examples above is sufficient. Otherwise, the Framework is unable to locate a View related to the given ViewModule. To resolve this issue, you need to decorate Views with the ViewType attribute to explicitly set the View-ViewModel relation.

csharp
[DevExpress.Utils.MVVM.UI.ViewType("AccountCollectionView")]
public partial class AccountsView { 
    // ...
}

[DevExpress.Utils.MVVM.UI.ViewType("CategoryCollectionView")]
public partial class CategoriesView { 
    // ...
}

[DevExpress.Utils.MVVM.UI.ViewType("TransactionCollectionView")]
    public partial class TransactionsView { 
    // ...
}
vb
<DevExpress.Utils.MVVM.UI.ViewType("AccountCollectionView")>
Partial Public Class AccountsView
    ' ...
End Class

<DevExpress.Utils.MVVM.UI.ViewType("CategoryCollectionView")>
Partial Public Class CategoriesView
    ' ...
End Class

<DevExpress.Utils.MVVM.UI.ViewType("TransactionCollectionView")>
Partial Public Class TransactionsView
    ' ...
End Class

Views in Separate Assemblies

When your Views are located in separate assemblies or have custom constructors, the ViewType attribute is not sufficient. In these cases, use one of the following approaches:

IViewService

Cast your navigation Service instance to the DevExpress.Utils.MVVM.UI.IViewService interface.

csharp
var service = DevExpress.Utils.MVVM.Services.DocumentManagerService.Create(tabbedView1);
var viewService = service as DevExpress.Utils.MVVM.UI.IViewService;
mvvmContext1.RegisterService(service);
vb
Dim service = DevExpress.Utils.MVVM.Services.DocumentManagerService.Create(tabbedView1)
Dim viewService = TryCast(service, DevExpress.Utils.MVVM.UI.IViewService)
mvvmContext1.RegisterService(service)

After that, handle the QueryView event to dynamically assign Views depending on the required View type.

csharp
viewService.QueryView += (s, e) =>
{
    if(e.ViewType == "View1")
        e.Result = new Views.View1();
    //...
};
vb
AddHandler viewService.QueryView, Sub(s, e)
    If e.ViewType = "View1" Then
        e.Result = New Views.View1()
    End If
    '...
End Sub

To specify which View type is required, you need to implement the corresponding logic in your navigation ViewModel. For instance, the code below enumerates all available Views as items within the Modules collection.

csharp
public class MyNavigationViewModel {
    protected IDocumentManagerService DocumentManagerService {
        get { return this.GetService<IDocumentManagerService>(); }
    }
    //Lists all available view types
    public string[] Modules {
        get { return new string[] { "View1", "View2", "View3" }; }
    }
    //Bind this command to required UI elements to create and display a document
    public void Show(string moduleName) {
        var document = DocumentManagerService.CreateDocument(moduleName, null, this);
        if(document != null) {
            document.Title = moduleName;
            document.Show();}
    }
}
vb
Public Class MyNavigationViewModel
    Protected ReadOnly Property DocumentManagerService() As IDocumentManagerService
        Get
            Return Me.GetService(Of IDocumentManagerService)()
        End Get
    End Property
    'Lists all available view types
    Public ReadOnly Property Modules() As String()
        Get
            Return New String() { "View1", "View2", "View3" }
        End Get
    End Property
    'Bind this command to required UI elements to create and display a document
    Public Sub Show(ByVal moduleName As String)
        Dim document = DocumentManagerService.CreateDocument(moduleName, Nothing, Me)
        If document IsNot Nothing Then
            document.Title = moduleName
            document.Show()
        End If
    End Sub
End Class

Control APIs

You can use an API of individual View controls that your navigation Service manages. For example, if Views should be displayed as DocumentManager tabs, handle the BaseView.QueryControl event to populate Documents. The View type is stored as the Document.ControlName property value.

csharp
var service = DevExpress.Utils.MVVM.Services.DocumentManagerService.Create(tabbedView1);
mvvmContext1.RegisterService(service);

tabbedView1.QueryControl += (s, e) =>
{
    if(e.Document.ControlName == "View 2")
        e.Control = new Views.View2();
    //...
};
vb
Dim service = DevExpress.Utils.MVVM.Services.DocumentManagerService.Create(tabbedView1)
mvvmContext1.RegisterService(service)

AddHandler tabbedView1.QueryControl, Sub(s, e)
    If e.Document.ControlName = "View 2" Then
        e.Control = New Views.View2()
    End If
    '...
End Sub

IViewLocator

All DevExpress navigation services use the DevExpress.Utils.MVVM.UI.IViewLocator service to find and manage required Views. The following code demonstrates how to implement a custom View Locator service:

csharp
public class ViewLocator : IViewLocator {
    object IViewLocator.Resolve(string name, params object[] parameters) {
        object viewModel = paremeters.Length==3 ? parameters[0] : null;
        object parameter = parameters.Length==3 ? parameters[1] : null;
        object parentViewModel = (paremeters.Length==3) ? paremeters[2] : paremeters[0] ;
        if(name == nameof(CustomersView))
            return new CustomersView()

        //...

        return null;
    }
}

You should register the View Locator service (locally or globally) to change the way it works with application Views:

csharp
// Registers the service globally (recommended).
DevExpress.Mvvm.ServiceContainer.Default.RegisterService(new ViewLocatorService());

When you register a custom IViewLocator service, DevExpress navigation services call the IViewLocator.Resolve method of your (custom) View Locator service to find and manage required Views:

csharp
protected IWindowService WindowService => this.GetService<IWindowService>();
WindowService.Title = "Document1";
// DevExpress MVVM Framework automatically calls the IViewLocator.Resolve method with specified parameters (you can create a View within this method).
WindowService.Show("Document1", "Parameter1", this);

Read the following topic for additional information on how to implement and register services: Services.

View and ViewModel Lifetime

Disposing a View also disposes the MvvmContext and ViewModel. You can either implement the IDisposable.Dispose method or bind a command to the View’s HandleDestroyed event to execute actions when the ViewModel is disposed.

csharp
// ViewModel
public ViewModel() {
    // Registers a new connection to the messenger.
    Messenger.Default.Register(...);
}
public void OnCreate() {
    // Captures UI-bound services.
    EnsureDispatcherService();
}
public void OnDestroy() {
    // Destroys a connection to the messanger.
    Messenger.Default.Unregister(...);
}
IDispatcherService dispatcher;
IDispatcherService EnsureDispatcherService() {
    return dispatcher ?? (dispatcher = this.GetRequiredService<IDispatcherService>());
}

// View (UserControl/Form)
fluent.WithEvent(this, nameof(HandleCreated)).EventToCommand(x => x.OnCreate);
fluent.WithEvent(this, nameof(HandleDestroyed)).EventToCommand(x => x.OnDestroy);
vb
Public Sub New()
    ' Registers a new connection to the messenger.
    Messenger.Default.Register(...)
End Sub
Public Sub OnCreate()
    ' Captures UI-bound services.
    EnsureDispatcherService()
End Sub
Public Sub OnDestroy()
    ' Destroys a connection to the messanger.
    Messenger.Default.Unregister(...)
End Sub
Private dispatcher As IDispatcherService
Private Function EnsureDispatcherService() As IDispatcherService
    If dispatcher IsNot Nothing Then
        Return dispatcher
    Else
        dispatcher = Me.GetRequiredService(Of IDispatcherService)()
        Return dispatcher
    End If
End Function

' View (UserControl/Form)
fluent.WithEvent(Me, nameof(HandleCreated)).EventToCommand(Function(x) x.OnCreate)
fluent.WithEvent(Me, nameof(HandleDestroyed)).EventToCommand(Function(x) x.OnDestroy)