我正在继续学习WPF,目前专注于MVVM并使用Karl Shifflett的“MVVM In a Box”教程 . 但是有一个关于在views / viewmodels之间共享数据以及它如何在屏幕上更新视图的问题 . 附:我还没有报道过IOC .
下面是我在测试应用程序中的MainWindow的屏幕截图 . 它分为3个部分(视图),一个 Headers ,一个带按钮的滑动面板,其余部分作为应用程序的主视图 . 应用程序的目的很简单,登录到应用程序 . 在成功登录后,登录视图应该被新视图(即OverviewScreenView)替换而消失,应用程序幻灯片上的相关按钮应该可见 .
我认为应用程序有2个ViewModel . 一个用于MainWindowView,另一个用于LoginView,因为MainWindow不需要具有Login命令,所以我将它保持独立 .
由于我还没有介绍过IOC,我创建了一个LoginModel类,它是一个单例 . 它只包含一个属性“public bool LoggedIn”,以及一个名为UserLoggedIn的事件 .
MainWindowViewModel构造函数注册到事件UserLoggedIn . 现在在LoginView中,当用户在LoginView上单击Login时,它会在LoginViewModel上引发一个命令,如果正确输入了用户名和密码,则会调用LoginModel并将LoggedIn设置为true . 这会导致UserLoggedIn事件触发,在MainWindowViewModel中处理该事件以使视图隐藏LoginView并用不同的视图(即概览屏幕)替换它 .
Questions
Q1 . 明显的问题,就是这样登录正确使用MVVM . 即控制流程如下 . LoginView - > LoginViewViewModel - > LoginModel - > MainWindowViewModel - > MainWindowView .
Q2 . 假设用户已登录,并且MainWindowViewModel已处理该事件 . 您将如何创建新视图并将其放在LoginView所在的位置,同样如何在不需要时处理LoginView . MainWindowViewModel中是否存在类似“UserControl currentControl”的属性,该属性设置为LoginView或OverviewScreenView .
Q3 . MainWindow是否应该在visual studio设计器中设置LoginView . 或者它应该留空,并以编程方式确认没有人登录,因此一旦加载MainWindow,它就会创建一个LoginView并在屏幕上显示它 .
下面的一些代码示例是否有助于回答问题
XAML for the MainWindow
<Window x:Class="WpfApplication1.MainWindow"
xmlns:local="clr-namespace:WpfApplication1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="372" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<local:HeaderView Grid.ColumnSpan="2" />
<local:ButtonsView Grid.Row="1" Margin="6,6,3,6" />
<local:LoginView Grid.Column="1" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</Window>
MainWindowViewModel
using System;
using System.Windows.Controls;
using WpfApplication1.Infrastructure;
namespace WpfApplication1
{
public class MainWindowViewModel : ObservableObject
{
LoginModel _loginModel = LoginModel.GetInstance();
private UserControl _currentControl;
public MainWindowViewModel()
{
_loginModel.UserLoggedIn += _loginModel_UserLoggedIn;
_loginModel.UserLoggedOut += _loginModel_UserLoggedOut;
}
void _loginModel_UserLoggedOut(object sender, EventArgs e)
{
throw new NotImplementedException();
}
void _loginModel_UserLoggedIn(object sender, EventArgs e)
{
throw new NotImplementedException();
}
}
}
LoginViewViewModel
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Input;
using WpfApplication1.Infrastructure;
namespace WpfApplication1
{
public class LoginViewViewModel : ObservableObject
{
#region Properties
private string _username;
public string Username
{
get { return _username; }
set
{
_username = value;
RaisePropertyChanged("Username");
}
}
#endregion
#region Commands
public ICommand LoginCommand
{
get { return new RelayCommand<PasswordBox>(LoginExecute, pb => CanLoginExecute()); }
}
#endregion //Commands
#region Command Methods
Boolean CanLoginExecute()
{
return !string.IsNullOrEmpty(_username);
}
void LoginExecute(PasswordBox passwordBox)
{
string value = passwordBox.Password;
if (!CanLoginExecute()) return;
if (_username == "username" && value == "password")
{
LoginModel.GetInstance().LoggedIn = true;
}
}
#endregion
}
}
1 回答
神圣的长问题,蝙蝠侠!
Q1: 该过程可行,但我不知道如何使用
LoginModel
与_1174115进行对话 .你可以尝试像
LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView
这样的东西我知道单身人士被认为是反模式,但我觉得这对于像这样的情况最容易 . 这样,单例类可以实现
INotifyPropertyChanged
接口,并在检测到login \ out事件时引发事件 .在
LoginViewModel
或Singleton上实现LoginCommand
(就个人而言,我可能会在ViewModel
上实现这一点,以在ViewModel和"back-end"实用程序类之间添加一定程度的分隔) . 此login命令将调用单例上的方法来执行登录 .Q2: 在这些情况下,我通常有(又一个)单例类充当
PageManager
或ViewModelManager
. 此类负责创建,处理和保持对顶级页面或CurrentPage的引用(仅限单页情况) .我的
ViewModelBase
类还有一个属性来保存显示我的类的UserControl的当前实例,这样我就可以挂钩Loaded和Unloaded事件 . 这使我能够拥有可以在_1174127中定义的虚拟OnLoaded(), OnDisplayed() and OnClosed()
方法,以便页面可以执行加载和卸载操作 .当MainWindowView显示
ViewModelManager.CurrentPage
实例时,一旦此实例发生更改,Unloaded事件将触发,我的页面的Dispose方法将被调用,最终GC
进入,其余部分将整齐 .Q3: 我不确定我是否理解这一个,但希望你的意思是"Display login page when user not logged in",如果是这种情况,你可以指示你的
ViewModelToViewConverter
在用户没有登录时忽略任何指令(通过检查SecurityContext单例)而不是只显示LoginView
模板,这也有帮助如果您希望页面只有某些用户有权查看或使用,您可以在构建View之前检查安全要求,并将其替换为安全提示 .对不起,答案很长,希望这有帮助:)
编辑:另外,你拼错了“管理”
Edit for questions in comments
对不起,澄清一下 - 我不是说LoginManager直接与MainWindowView交互(因为这应该只是一个视图),而是
LoginManager
只是设置一个CurrentUser属性来响应LoginCommand所做的调用,反过来引发PropertyChanged事件,MainWindowView(正在监听更改)会相应地做出反应 .然后,LoginManager可以调用
PageManager.Open(new OverviewScreen())
(或实现IOC时PageManager.Open("overview.screen")
),例如将用户重定向到用户登录后看到的默认屏幕 .LoginManager本质上是实际登录过程的最后一步,View只是适当地反映了这一点 .
此外,在键入此内容时,我发现不是拥有一个LoginManager单例,而是所有这些都可以放在
PageManager
类中 . 只需一个Login(string, string)
方法,即成功登录时设置CurrentUser .我不会将PageManager设计为View-ViewModel设计,只需要实现
INotifyPropertyChanged
的普通家用单例就行了,这样MainWindowView就可以对更改CurrentPage属性做出反应 .是 . 我使用这个类作为我所有ViewModel的基类 .
这个类包含
在所有页面上使用的属性,例如Title,PageKey和OverriddenUserContext .
常见的虚拟方法,如PageLoaded,PageDisplayed,PageSaved和PageClosed
实现INPC并公开受保护的OnPropertyChanged方法以用于引发PropertyChanged事件
并提供与页面交互的框架命令,如ClosePageCommand,SavePageCommand等 .
就个人而言,我只会持有当前正在显示的ViewModelBase的实例 . 然后由ContentControl中的MainWindowView引用它,如下所示:
Content="{Binding Source={x:Static vm:PageManager.Current}, Path=CurrentPage}"
.然后我还使用转换器将ViewModelBase实例转换为UserControl,但这纯粹是可选的;您可以只依赖ResourceDictionary条目,但此方法还允许开发人员拦截调用并在需要时显示SecurityPage或ErrorPage .
您可以设计应用程序,以便向用户显示的第一个页面是OverviewScreen的实例 . 其中,由于PageManager当前具有null的CurrentUser属性,ViewModelToViewConverter将拦截此而不是显示OverviewScreenView UserControl,而是显示LoginView UserControl .
如果用户成功登录,LoginViewModel将指示PageManager重定向到原始的OverviewScreen实例,这次正确显示,因为CurrentUser属性为非null .
我和你在一起,我喜欢我一个好的单身人士 . 但是,这些的使用应限于仅在必要时使用 . 但是在我看来它们确实有完全有效的用途,但不确定是否还有其他人想要在这件事情上加入?
Edit 2:
不,我正在使用我在过去12个月左右创建和改进的框架 . 该框架仍然遵循大多数MVVM指南,但包括一些个人触摸,减少了编写所需的整体代码量 .
例如,一些MVVM示例就像你一样 Build 了他们的观点;而View在其ViewObject.DataContext属性中创建ViewModel的新实例 . 这可能适用于某些人,但不允许开发人员从ViewModel挂钩某些Windows事件,如OnPageLoad() .
我的案例中的OnPageLoad()是在页面上的所有控件都已创建后调用的,并且可以在调用构造函数后的几分钟内立即进入屏幕查看,或者根本不可以 . 例如,如果该页面在当前未选中的选项卡内有多个子页面,那么我可以在此处执行大部分数据加载以加快页面加载过程 .
但不仅如此,通过以这种方式创建ViewModel,每个View中的代码量增加了至少三行 . 这听起来可能不是很多,但是这些代码行不仅对于创建重复代码的 all 视图基本相同,而且如果您的应用程序需要许多视图,则额外的行数可以非常快地加起来 . 那,我'm really lazy.. I didn't成为开发人员键入代码 .
在这种情况下,PageManager不需要直接引用每个打开的ViewModelBase类,只需要那些顶级的 . 所有其他页面都将是其父级的子级,以便您更好地控制层次结构,并允许您逐步删除“保存”和“关闭”事件 .
如果将它们放在PageManager中的
ObservableCollection<ViewModelBase>
属性中,则只需要创建MainWindow 's TabControl so that it'的ItemsSource属性指向PageManager上的Children属性,并让WPF引擎完成剩下的工作 .当然,为了给你一个大纲,显示一些代码会更容易 .
通过以下部分阅读此代码,内容如下:
如果value为null,则返回 . 简单的空引用检查 .
如果该值是ViewModelBase,并且该页面已加载,则只返回该View . 如果不这样做,则每次显示页面时都会创建一个新视图,这会导致一些意外行为 .
获取页面模板UserControl(如下所示)
设置PageTemplate属性,以便可以挂钩此实例,因此我们不会在每次传递时加载新实例 .
将View DataContext设置为ViewModel实例,这两行完全取代了我之前从每个视图中讨论的那三行 .
返回模板 . 然后,它将显示在ContentPresenter中供用户查看 .
这是转换器中执行大部分繁重工作的代码,通读您可以看到的部分:
主try..catch块用于捕获任何类构造错误,包括,
页面不存在,
构造函数代码中的运行时错误,
和XAML中的致命错误 .
convertViewModelTypeToViewType()只是试图找到与ViewModel对应的View,并返回它认为应该是的类型代码(这可能为null) .
如果这不为null,则创建该类型的新实例 .
如果我们找不到要使用的View,请尝试为该ViewModel类型创建默认页面 . 我还有一些从ViewModelBase继承的ViewModel基类,它们提供了页面类型之间的职责分离 .
例如,SearchablePage类将仅显示特定类型系统中所有对象的列表,并提供“添加”,“编辑”,“刷新”和“筛选”命令 .
MaintenancePage将从数据库中检索完整对象,动态生成和定位对象公开的字段的控件,根据对象拥有的任何集合创建子页面,并提供要使用的保存和删除命令 .
如果我们仍然没有要使用的模板,请抛出错误,以便开发人员知道出现了问题 .
在catch块中,发生的任何运行时错误都会在友好的ErrorPage中显示给用户 .
这一切都允许我专注于仅创建ViewModel类,因为应用程序将简单地显示默认页面,除非View页面已由开发人员为该ViewModel显式覆盖 .