windowsforms-114173-cross-platform-app-development-winforms-mvvm-concepts-view-management.md
This topic explains how to implement navigation between separate application Views, and build the View-ViewModel relations.
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:
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):
Declare a property inside a ViewModel to retrieve an instance of a registered Service.
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.
// View
mvvmContext1.ViewModelType = typeof(mvvmNavi.Form1ViewModel);
// ViewModel
[POCOViewModel()]
public class Form1ViewModel {
//...
}
' 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.
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 {
}
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:
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:
public Form1() {
InitializeComponent();
//. . .
var service = DocumentManagerService.Create(tabbedView1);
service.UseDeferredLoading = DevExpress.Utils.DefaultBoolean.True;
mvvmContext1.RegisterDefaultService(service);
}
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:
[POCOViewModel()]
public class Form1ViewModel {
protected IDocumentManagerService DocumentManagerService {
get { return this.GetService<IDocumentManagerService>(); }
}
}
<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.
// 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();
}
' 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
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:
// main View
var service = NavigationService.Create(navigationFrame1);
mvvmContext1.RegisterDefaultService(service);
' main View
Dim service = NavigationService.Create(navigationFrame1)
mvvmContext1.RegisterDefaultService(service)
The property that retrieves a Service instance:
// main ViewModel
protected INavigationService NavigationService {
get { return this.GetService<INavigationService>(); }
}
' main ViewModel
Protected ReadOnly Property NavigationService() As INavigationService
Get
Return Me.GetService(Of INavigationService)()
End Get
End Property
Navigation:
// 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);
}
' 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
In this example, child Views are shown as separate forms above other application windows. To do this, use the WindowedDocumentManagerService Service.
Local registration:
// main View
var service = WindowedDocumentManagerService.Create(mainView);
service.DocumentShowMode = WindowedDocumentManagerService.FormShowMode.Dialog;
mvvmContext.RegisterService(service);
' main View
Dim service = WindowedDocumentManagerService.Create(mainView)
service.DocumentShowMode = WindowedDocumentManagerService.FormShowMode.Dialog
mvvmContext.RegisterService(service)
The property that retrieves a Service instance:
// main ViewModel
protected IDocumentManagerService WindowedDocumentManagerService {
get { return this.GetService<IDocumentManagerService>(); }
}
' main ViewModel
Protected ReadOnly Property WindowedDocumentManagerService() As IDocumentManagerService
Get
Return Me.GetService(Of IDocumentManagerService)()
End Get
End Property
Navigation:
// 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();
}
' 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:
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 */
}
}
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
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.
[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 {
// ...
}
<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
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:
Cast your navigation Service instance to the DevExpress.Utils.MVVM.UI.IViewService interface.
var service = DevExpress.Utils.MVVM.Services.DocumentManagerService.Create(tabbedView1);
var viewService = service as DevExpress.Utils.MVVM.UI.IViewService;
mvvmContext1.RegisterService(service);
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.
viewService.QueryView += (s, e) =>
{
if(e.ViewType == "View1")
e.Result = new Views.View1();
//...
};
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.
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();}
}
}
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
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.
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();
//...
};
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
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:
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:
// 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:
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.
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.
// 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);
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)