我将继续学习WPF,目前主要关注MVVM,并使用Karl Shifflett的“盒子中的MVVM”教程。但是有一个关于在 View / View 模型之间共享数据以及如何更新屏幕上的 View 的问题。 ps我还没有介绍IOC。
下面是我在测试应用程序中的MainWindow的屏幕截图。它分为3个部分( View ),一个标题,一个带按钮的滑动面板,其余作为应用程序的主 View 。该应用程序的目的很简单,登录到该应用程序。成功登录后,登录 View 应消失,而将其替换为新 View (即OverviewScreenView),并且应用程序幻灯片上的相关按钮应变为可见。
我认为该应用程序具有2个ViewModel。一种用于MainWindowView,一种用于LoginView,因为MainWindow不需要具有用于登录的命令,因此我将其分开。
由于我还没有介绍IOC,所以我创建了一个Singleton的LoginModel类。它仅包含一个属性“public bool LoggedIn”和一个名为UserLoggedIn的事件。
MainWindowViewModel构造函数将注册到事件UserLoggedIn。现在在LoginView中,当用户单击LoginView上的Login时,它将在LoginViewModel上引发一个命令,如果正确输入用户名和密码,则该命令将调用LoginModel并将LoggedIn设置为true。这会引发UserLoggedIn事件,该事件在MainWindowViewModel中进行处理,从而导致该 View 隐藏LoginView并将其替换为其他 View (即概述屏幕)。
问题
Q1。一个明显的问题是,这样登录是对MVVM的正确使用。即控制流程如下。 LoginView-> LoginViewViewModel-> LoginModel-> MainWindowViewModel-> MainWindowView。
Q2。假设用户已经登录,并且MainWindowViewModel已处理该事件。您将如何创建一个新的View并将其放置在LoginView所在的位置,同样,一旦不需要它,您将如何处置LoginView。 MainWindowViewModel中是否会有诸如“UserControl currentControl”之类的属性,该属性将设置为LoginView或OverviewScreenView。
Q3。应该在Visual Studio设计器中设置MainWindow的LoginView吗。或者应将其保留为空白,并以编程方式意识到没有人登录,因此一旦加载MainWindow,便会创建一个LoginView并将其显示在屏幕上。
下面的一些代码示例,如果有助于回答问题
用于MainWindow的XAML
<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
}
}
最佳答案
长久的问题, bat 侠!
Q1:
该过程将正常工作,但是我不知道如何使用LoginModel
与MainWindowViewModel
对话。
您可以尝试类似LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView
我知道单例被某些人视为反模式,但是我发现这种情况最简单。这样,单例类可以实现INotifyPropertyChanged
接口(interface),并在检测到login\out事件时引发事件。
在LoginCommand
或Singleton上实现LoginViewModel
(就我个人而言,我可能会在ViewModel
上实现此功能,以在ViewModel和“后端”实用程序类之间添加一定程度的分隔)。此登录命令将在单例上调用方法以执行登录。
第2季:
在这些情况下,我通常(还有另一个)单例类充当PageManager
或ViewModelManager
。此类负责创建,处置和保存对顶级页面或CurrentPage的引用(仅在单页面的情况下)。
我的ViewModelBase
类还具有一个属性,用于保存显示我的类的UserControl的当前实例,因此可以挂接Loaded和Unloaded事件。这使我能够拥有可以在OnLoaded(), OnDisplayed() and OnClosed()
中定义的虚拟ViewModel
方法,以便页面可以执行加载和卸载操作。
当MainWindowView显示ViewModelManager.CurrentPage
实例时,一旦该实例发生更改,就会触发Unloaded事件,调用我页面的Dispose方法,最后GC
进入,其余部分保持整齐。
第三季度:
我不确定我是否理解这一点,但希望您的意思是“用户未登录时显示登录页面”,在这种情况下,您可以指示ViewModelToViewConverter
在用户未登录时忽略任何指令(通过检查SecurityContext单例)并仅显示LoginView
模板,这在您希望只有某些用户有权查看或使用的页面可以在构造View之前检查安全性要求并将其替换的情况下很有用。安全提示。
抱歉,答案很长,希望对您有所帮助:)
编辑:
另外,您拼错了“管理”
编辑评论中的问题
抱歉,需要澄清一下-我并不是说LoginManager直接与MainWindowView交互(因为这应该只是一个 View ),而是LoginManager
只是设置一个CurrentUser属性来响应LoginCommand的调用,依次引发PropertyChanged事件和MainWindowView(正在监听更改)做出相应的 react 。
然后,LoginManager可以调用PageManager.Open(new OverviewScreen())
(或在实现IOC时调用PageManager.Open("overview.screen")
),例如,将用户重定向到用户登录后看到的默认屏幕。
LoginManager本质上是实际登录过程的最后一步,而View恰好反射(reflect)了这一点。
另外,在键入此命令时,我想到,可以将所有这些都放置在PageManager
类中,而不是使用LoginManager单例。只需使用Login(string, string)
方法,该方法可在成功登录时设置CurrentUser。
我不会将PageManager设计为View-ViewModel设计,而只是实现INotifyPropertyChanged
的普通家用单例就可以解决问题,这样MainWindowView可以对CurrentPage属性的更改使用react。
是的。我将此类用作所有ViewModel的基类。
该类(class)包含
OverriddenUserContext。
就个人而言,我只会保留当前正在显示的ViewModelBase的实例。然后,由MainWindowView在ContentControl中进行引用,如下所示:
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,因此这次可以正确显示。
我和你在一起,我喜欢我一个好单例。但是,应将它们的使用限制为仅在必要时使用。但是我认为它们确实具有完全有效的用法,但是不确定是否有人愿意对此事进行干预?
编辑2:
不,我使用的是过去十二个月左右创建并完善的框架。该框架仍然遵循大多数MVVM准则,但是包含一些个人方面的内容,从而减少了需要编写的总体代码量。
例如,一些MVVM示例建立的 View 与您的 View 基本相同。而View在其ViewObject.DataContext属性内创建ViewModel的新实例。对于某些人来说,这可能工作得很好,但不允许开发人员从ViewModel挂钩某些Windows事件,例如OnPageLoad()。
在我的情况下,OnPageLoad()是在页面上的所有控件都已创建并进入屏幕后被调用的,这可能是立即的,在调用构造函数后的几分钟之内,或者根本没有。例如,如果该页面在选项卡中有多个子页面(当前未选中),则将在此处进行大部分数据加载,以加快页面加载过程。
不仅如此,通过以这种方式创建ViewModel,每个 View 中的代码量最少增加三行。这听起来可能并不多,但对于创建重复代码的所有 View ,这些代码行不仅对于本质上是相同的,而且如果您的应用程序需要很多 View ,那么额外的行数可能会很快增加。那,我真的很懒。.我没有成为打字代码的开发人员。
在这种情况下,PageManager不需要保留对每个打开的ViewModelBase类的直接引用,而仅需要保留对顶级类的直接引用。所有其他页面都是其父级的子级,以使您能够更好地控制层次结构并允许您滴入Save和Close事件。
如果将它们放在PageManager的
ObservableCollection<ViewModelBase>
属性中,则只需要创建MainWindow的TabControl,以便它的ItemsSource属性指向PageManager上的Children属性,并让WPF引擎完成其余的工作。当然,要给您一个大纲,显示一些代码会更容易。
public override object Convert(object value, SimpleConverterArguments args)
{
if (value == null)
return null;
ViewModelBase vm = value as ViewModelBase;
if (vm != null && vm.PageTemplate != null)
return vm.PageTemplate;
System.Windows.Controls.UserControl template = GetTemplateFromObject(value);
if (vm != null)
vm.PageTemplate = template;
if (template != null)
template.DataContext = value;
return template;
}
阅读以下部分的代码:
public static System.Windows.Controls.UserControl GetTemplateFromObject(object o)
{
System.Windows.Controls.UserControl template = null;
try
{
ViewModelBase vm = o as ViewModelBase;
if (vm != null && !vm.CanUserLoad())
return new View.Core.SystemPages.SecurityPrompt(o);
Type t = convertViewModelTypeToViewType(o.GetType());
if (t != null)
template = Activator.CreateInstance(t) as System.Windows.Controls.UserControl;
if (template == null)
{
if (o is SearchablePage)
template = new View.Core.Pages.Generated.ViewList();
else if (o is MaintenancePage)
template = new View.Core.Pages.Generated.MaintenancePage(((MaintenancePage)o).EditingObject);
}
if (template == null)
throw new InvalidOperationException(string.Format("Could not generate PageTemplate object for '{0}'", vm != null && !string.IsNullOrEmpty(vm.PageKey) ? vm.PageKey : o.GetType().FullName));
}
catch (Exception ex)
{
BugReporter.ReportBug(ex);
template = new View.Core.SystemPages.ErrorPage(ex);
}
return template;
}
这是转换器中执行大部分艰苦工作的代码,请仔细阅读以下内容:
所有这些使我可以只专注于创建ViewModel类,因为应用程序将简单地显示默认页面,除非开发人员已针对该ViewModel明确覆盖了View页面。
关于c# - WPF MVVM-简单登录到应用程序,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/7114687/