导航和视图管理
本主题解释了如何在不同的应用程序视图之间实现导航,以及如何构建View-ViewModel关系。
标准导航服务
DevExpress MVVM框架包括许多,您可以利用它们来实现不同应用模块(视图)之间的导航。
使用任何MVVM服务包括三个主要步骤:
1.在视图中注册服务时可以全局注册(它可以从任何应用程序视图中获得)或本地注册(如果您打算仅从此模块中使用它)。
2.在ViewModel中声明一个属性来检索已注册服务的实例。
C#:
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; } }
3.调用ViewModel中Service实例的公共API。
例如,主应用程序的视图有MvvmContext组件,它将主应用程序的表单(视图)链接到“Form1ViewModel”ViewModel。
C#:
// View mvvmContext1.ViewModelType = typeof(mvvmNavi.Form1ViewModel); // ViewModel [POCOViewModel()] public class Form1ViewModel { //... }
VB.NET:
' 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
该应用程序还有两个UserControl,每个都有自己的MvvmContext组件,UserControl的视图链接到它相应的ViewModel。
C#:
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.NET:
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
提示:上面的代码初始化了MvvmContext组件,并设置了它们的ViewModelType属性,只是为了举例说明。在实际的应用程序中,建议在设计时将组件放在 Forms 和 UserControls上,并使用智能标签菜单来设置ViewModels。
下面的例子说明了如何根据您的任务选择和使用不同的DevExpress服务:
例1:DocumentManager选项卡
主应用程序表单(视图)有一个空的文档管理器,任务是将UserControls A和B显示为DocumentManager选项卡(文档)。
要管理DocumentManager文档,请使用并在主视图中注册它:
C#:
public Form1() { InitializeComponent(); //. . . var service = DocumentManagerService.Create(tabbedView1); service.UseDeferredLoading = DevExpress.Utils.DefaultBoolean.True; mvvmContext1.RegisterDefaultService(service); }
VB.NET:
Public Sub Form1() InitializeComponent() '. . . Dim service = DocumentManagerService.Create(tabbedView1) service.UseDeferredLoading = DevExpress.Utils.DefaultBoolean.True mvvmContext1.RegisterDefaultService(service) End Sub
在主ViewModel中,实现一个属性来检索注册服务的实例:
C#:
[POCOViewModel()] public class Form1ViewModel { protected IDocumentManagerService DocumentManagerService { get { return this.GetService<IDocumentManagerService>(); } } }
VB.NET:
<POCOViewModel()> Public Class Form1ViewModel Protected ReadOnly Property DocumentManagerService() As IDocumentManagerService Get Return Me.GetService(Of IDocumentManagerService)() End Get End Property End Class
DocumentManagerService.CreateDocument和DocumentManagerService.FindDocumentById方法允许您创建和定位文档,然后可以调用IDocument.Show方法来显示它们。
C#:
// 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.NET:
' 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
这个核心方法可以在各种场景中使用。
- 创建一个带有特定UserControl的新文档,并在应用程序启动时加载它:
C#:
// 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);
VB.NET:
' main ViewModel Private ReadOnly Shared ViewA_ID As New Object() Private ReadOnly Shared ViewB_ID As New Object() Public Sub CreateDocumentA() CreateDocument(ViewA_ID, "ViewA", "UserControl A") End Sub Public Sub CreateDocumentB() CreateDocument(ViewB_ID, "ViewB", "UserControl B") End Sub ' main View Dim fluent = mvvmContext1.OfType(Of Form1ViewModel)() fluent.WithEvent(Me, "Load").EventToCommand(Function(x) x.CreateDocumentA)
为每个UserControl创建一个文档,并在启动时加载所有这些文档。
C#:
// 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);
VB.NET:
' main ViewModel Public Sub CreateAllDocuments() CreateDocument(ViewA_ID, "ViewA", "UserControl A") CreateDocument(ViewB_ID, "ViewB", "UserControl B") End Sub ' main View Dim fluent = mvvmContext1.OfType(Of Form1ViewModel)() fluent.WithEvent(Me, "Load").EventToCommand(Function(x) x.CreateAllDocuments)
- 将UI元素(例如,Ribbon按钮)绑定到一个命令,该命令创建一个具有特定UserControl的新文档。
C#:
// 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);
VB.NET:
' main ViewModel Public Sub CreateDocument(ByVal id As Object, ByVal documentType As String, ByVal title As String) Dim document = DocumentManagerService.CreateDocument(documentType, parameter:= Nothing, parentViewModel:=Me) document.Id = id document.Title = title document.Show() End Sub Public Sub CreateDocumentA() CreateDocument(New Object(), "ViewA", "UserControl A") End Sub Public Sub CreateDocumentB() CreateDocument(New Object(), "ViewB", "UserControl B") End Sub ' main View fluent.BindCommand(bbiCreateDocA, Function(x) x.CreateDocumentA) fluent.BindCommand(bbiCreateDocB, Function(x) x.CreateDocumentB)
示例2:导航框架
主表单(视图)有一个空的NavigationFrame组件,该组件可以存储多个页面,但一次只允许用户查看一个页面。要用页面填充该组件并实现导航,请使用NavigationService。
- 全球服务注册:
C#:
// main View var service = NavigationService.Create(navigationFrame1); mvvmContext1.RegisterDefaultService(service);
VB.NET:
' main View Dim service = NavigationService.Create(navigationFrame1) mvvmContext1.RegisterDefaultService(service)
- 检索Service实例的属性:
C#:
// main ViewModel protected INavigationService NavigationService { get { return this.GetService<INavigationService>(); } }
VB.NET:
' main ViewModel Protected ReadOnly Property NavigationService() As INavigationService Get Return Me.GetService(Of INavigationService)() End Get End Property
- 导航:
C#:
// 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.NET:
' 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)
Navigate方法可以接受参数作为它的第二个参数,这允许您在导航模块之间传递任何数据。DevExpress Demo Center示例演示了如何将先前活动模块的名称传递给当前选择的视图,注意在这个例子中,全局服务注册允许每个子ViewModel使用这个服务的API。
示例3:情态形式
在本例中,子视图在其他应用程序窗口上方显示为单独的表单。要做到这一点,请使用WindowedDocumentManagerService服务。
- 本地注册:
C#:
// main View var service = WindowedDocumentManagerService.Create(mainView); service.DocumentShowMode = WindowedDocumentManagerService.FormShowMode.Dialog; mvvmContext.RegisterService(service);
VB.NET:
' main View Dim service = WindowedDocumentManagerService.Create(mainView) service.DocumentShowMode = WindowedDocumentManagerService.FormShowMode.Dialog mvvmContext.RegisterService(service)
- 检索Service实例的属性:
C#:
// main ViewModel protected IDocumentManagerService WindowedDocumentManagerService { get { return this.GetService<IDocumentManagerService>(); } }
VB.NET:
' main ViewModel Protected ReadOnly Property WindowedDocumentManagerService() As IDocumentManagerService Get Return Me.GetService(Of IDocumentManagerService)() End Get End Property
- 导航:
C#:
// 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.NET:
' 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
- 结束情态形式:
C#:
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.NET:
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
ViewType属性
如果您遵循命名约定(“ModuleX”视图的ViewModel被称为“ModuleXViewModel”),并且视图/ViewModel位于相同的命名空间中,则上述示例中显示的MVVM服务的默认使用就足够了,否则框架将无法定位与给定ViewModule相关的视图。要解决这个问题,需要用ViewType属性修饰Views,来显式地设置View-ViewModel关系。
C#:
[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.NET:
<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
单独程序集中的视图
当视图位于单独的程序集中或具有自定义构造函数时,ViewType属性是不够的。在这些情况下,请使用以下方法之一:
IViewService
将导航服务实例转换为DevExpress.Utils.MVVM.UI.IViewService接口。
C#:
var service = DevExpress.Utils.MVVM.Services.DocumentManagerService.Create(tabbedView1); var viewService = service as DevExpress.Utils.MVVM.UI.IViewService; mvvmContext1.RegisterService(service);
VB.NET:
Dim service = DevExpress.Utils.MVVM.Services.DocumentManagerService.Create(tabbedView1) Dim viewService = TryCast(service, DevExpress.Utils.MVVM.UI.IViewService) mvvmContext1.RegisterService(service)
之后,处理QueryView事件并根据所需的视图类型动态分配视图。
C#:
viewService.QueryView += (s, e) => { if(e.ViewType == "View1") e.Result = new Views.View1(); //... };
VB.NET:
AddHandler viewService.QueryView, Sub(s, e) If e.ViewType = "View1" Then e.Result = New Views.View1() End If '... End Sub
要指定需要哪种视图类型,您需要在导航ViewModel中实现相应的逻辑。例如,下面的代码将所有可用的视图枚举为Modules集合中的项。
C#:
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.NET:
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
控制APIs
您可以使用导航服务管理的单个视图控件的API。例如,如果视图应该显示为DocumentManager选项卡,便处理BaseView.QueryControl事件来填充文档,View类型存储Document.ControlName属性值。
C#:
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.NET:
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
所有DevExpress导航服务都使用DevExpress.Utils.MVVM.UI.IViewLocator服务来查找和管理所需的视图,您可以创建此服务的自定义实现并注册它(本地或全局)来更改它与应用程序视图的工作方式。请参阅本文了解如何实现和注册自定义服务:services。
视图和视图模型生存期
处置视图也处置MvvmContext和ViewModel,您既可以实现IDisposable.Dispose方法,也可以将命令绑定到视图的HandleDestroyed事件,以便在ViewModel被处置时执行操作。
C#:
// 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.NET:
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)