Skip to main content

Navigation and View Management

  • 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):

    // Registers a global service.
    DevExpress.Mvvm.ServiceContainer.Default.RegisterService(new SomeService());
    // Registeres a local service.
    serviceContainer.RegisterService(new SomeFilterService(ModuleType.MyCustomFilter));
    

    Learn more about Services

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

    [POCOViewModel()]
    public class Form1ViewModel {
        protected IDocumentManagerService DocumentManagerService {
            get { return this.GetService<IDocumentManagerService>(); }
        }
    }
    
  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.

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


// ViewModel
[POCOViewModel()]
public class Form1ViewModel {
    //...
}

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 {
}

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:

public Form1() {
    InitializeComponent();
    //. . .
    var service = DocumentManagerService.Create(tabbedView1);
    service.UseDeferredLoading = DevExpress.Utils.DefaultBoolean.True;
    mvvmContext1.RegisterDefaultService(service);
}

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>(); }
    }
}

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();
}

This core method can be used in various scenarios.

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

    // main ViewModel
    readonly static object ViewA_ID = new object();
    readonly static object ViewB_ID = new object();
    
    public void CreateDocumentA() {
        CreateDocument(ViewA_ID, "ViewA", "UserControl A");
    }
    
    public void CreateDocumentB() {
        CreateDocument(ViewB_ID, "ViewB", "UserControl B");
    }
    
    // main View
    var fluent = mvvmContext1.OfType<Form1ViewModel>();
    fluent.WithEvent(this, "Load").EventToCommand(x => x.CreateDocumentA);
    
  • Create one Document for each UserControl and load all of these Documents at startup.

    // main ViewModel
    public void CreateAllDocuments() {
        CreateDocument(ViewA_ID, "ViewA", "UserControl A");
        CreateDocument(ViewB_ID, "ViewB", "UserControl B");
    }
    
    // main View
    var fluent = mvvmContext1.OfType<Form1ViewModel>();
    fluent.WithEvent(this, "Load").EventToCommand(x => x.CreateAllDocuments);
    
  • Bind UI elements (for instance, Ribbon buttons) to a command that creates a new Document with the specific UserControl.

    // main ViewModel
    public void CreateDocument(object id, string documentType, string title) {
        var document = DocumentManagerService.CreateDocument(
            documentType, parameter: null, parentViewModel: this);
        document.Id = id;
        document.Title = title;
        document.Show();
    }
    
    public void CreateDocumentA() {
        CreateDocument(new object(), "ViewA", "UserControl A");
    }
    
    public void CreateDocumentB() {
        CreateDocument(new object(), "ViewB", "UserControl B");
    }
    
    // main View
    fluent.BindCommand(bbiCreateDocA, x => x.CreateDocumentA);
    fluent.BindCommand(bbiCreateDocB, x => x.CreateDocumentB);
    

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:

// main View
var service = NavigationService.Create(navigationFrame1);
mvvmContext1.RegisterDefaultService(service);

The property that retrieves a Service instance:

// main ViewModel
protected INavigationService NavigationService {
    get { return this.GetService<INavigationService>(); }
}

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);
}

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:

// main View
var 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>(); }
}

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();
}

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 */
    }
}

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.

[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 { 
    // ...
}

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.

var service = DevExpress.Utils.MVVM.Services.DocumentManagerService.Create(tabbedView1);
var viewService = service as 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();
    //...
};

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();}
    }
}

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.

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();
    //...
};

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:

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.

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.

// 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);