我试图学习WPF和MVVM问题,但遇到了障碍。
这个问题与这个问题类似,但不完全相同(handling-dialog -in-wpf-with-mvvm)…
我有一个使用MVVM模式编写的“Login”表单。
该表单有一个ViewModel,其中包含用户名和密码,它们使用普通的数据绑定绑定到XAML中的视图。
它还有一个“登录”命令,该命令被绑定到表单上的“登录”按钮,agan使用正常的数据绑定。
当“Login”命令触发时,它会调用ViewModel中的一个函数,该函数会触发并通过网络发送数据以登录。当这个函数完成时,有2个动作:
登录无效-我们只是显示一个消息框,一切正常
登录是有效的,我们需要关闭login表单,并让它返回true作为它的dialgresult…
问题是,ViewModel对实际的视图一无所知,所以它如何关闭视图并告诉它返回一个特定的dialgresult ??我可以在代码背后粘贴一些代码,和/或通过视图模型传递视图,但这似乎会击败整个点MVVM完全…
更新
最后,我违背了MVVM模式的“纯度”,让视图发布了一个Closed事件,并公开了一个Close方法。ViewModel会调用view。close。视图仅通过接口了解,并通过IOC容器连接,因此不会损失可测试性或可维护性。
公认的答案是-5票,这似乎相当愚蠢!虽然我很清楚通过“纯粹”解决问题所获得的良好感觉,但我肯定不是唯一一个认为200行事件、命令和行为只是为了避免以“模式”和“纯粹”为名的一行方法有点荒谬的人....
我实现了Joe White的解决方案,但遇到了偶尔出现的“只有在创建Window并显示为对话框后才能设置dialgresult”错误。
在视图关闭后,我一直保持ViewModel,偶尔我也会使用相同的VM打开一个新的视图。看起来,在旧视图被垃圾收集之前关闭新视图会导致dialgresultchanged试图在关闭的窗口上设置dialgresult属性,从而引发错误。
我的解决方案是改变dialgresultchanged来检查窗口的IsLoaded属性:
private static void DialogResultChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var window = d as Window;
if (window != null && window.IsLoaded)
window.DialogResult = e.NewValue as bool?;
}
做出此更改后,关闭对话框的任何附件都将被忽略。
以下是我最初所做的,它确实有效,但它似乎相当冗长和丑陋(全局静态的任何东西都不好)
1: App.xaml.cs
public partial class App : Application
{
// create a new global custom WPF Command
public static readonly RoutedUICommand LoggedIn = new RoutedUICommand();
}
2: LoginForm.xaml
// bind the global command to a local eventhandler
<CommandBinding Command="client:App.LoggedIn" Executed="OnLoggedIn" />
3: LoginForm.xaml.cs
// implement the local eventhandler in codebehind
private void OnLoggedIn( object sender, ExecutedRoutedEventArgs e )
{
DialogResult = true;
Close();
}
4: LoginFormViewModel.cs
// fire the global command from the viewmodel
private void OnRemoteServerReturnedSuccess()
{
App.LoggedIn.Execute(this, null);
}
我后来删除了所有这些代码,只让LoginFormViewModel在它的视图上调用Close方法。它最终变得更好,也更容易理解。在我看来,模式的意义在于让人们更容易理解你的应用程序在做什么,在这种情况下,MVVM使它比我没有使用它更难理解,而且现在是一个反模式。
这里有很多关于MVVM利弊的评论。就我而言,我同意Nir的观点;这是一个适当使用模式的问题,MVVM并不总是适合。人们似乎愿意牺牲所有最重要的软件设计原则,只是为了让它适合MVVM。
也就是说,. .我觉得你的案子可以做点重构。
在我遇到的大多数情况下,WPF可以让你在没有多个Windows的情况下工作。也许你可以尝试使用框架和页面来代替带有对话框的Windows。
在你的情况下,我的建议是让LoginFormViewModel处理LoginCommand,如果登录无效,将LoginFormViewModel上的属性设置为适当的值(false或一些枚举值,如UserAuthenticationStates.FailedAuthentication)。对于成功登录(true或其他enum值),也可以执行相同的操作。然后使用DataTrigger来响应各种用户身份验证状态,并可以使用简单的Setter来更改Frame的Source属性。
让你的登录窗口返回一个对话框,我认为这是你感到困惑的地方;dialgresult实际上是ViewModel的一个属性。在我使用WPF的有限经验中,当某些事情感觉不对时,通常是因为我在考虑如何在WinForms中做同样的事情。
希望这能有所帮助。
我读了所有的答案,但我必须说,他们中的大多数都不够好,甚至更糟。
你可以用DialogService类很好地处理这个问题,它的职责是显示对话框窗口并返回对话框结果。我已经创建示例项目演示它的实现和使用。
以下是最重要的部分:
//we will call this interface in our viewmodels
public interface IDialogService
{
bool? ShowDialog(object dialogViewModel, string caption);
}
//we need to display logindialog from mainwindow
public class MainWindowViewModel : ViewModelBase
{
public string Message {get; set;}
public void ShowLoginCommandExecute()
{
var loginViewModel = new LoginViewModel();
var dialogResult = this.DialogService.ShowDialog(loginViewModel, "Please, log in");
//after dialog is closed, do someting
if (dialogResult == true && loginViewModel.IsLoginSuccessful)
{
this.Message = string.Format("Hello, {0}!", loginViewModel.Username);
}
}
}
public class DialogService : IDialogService
{
public bool? ShowDialog(object dialogViewModel, string caption)
{
var contentView = ViewLocator.GetView(dialogViewModel);
var dlg = new DialogWindow
{
Title = caption
};
dlg.PART_ContentControl.Content = contentView;
return dlg.ShowDialog();
}
}
这样不是更简单吗?比EventAggregator或其他类似的解决方案更简单,更可读和持久,但并非最不容易调试?
正如你所看到的,在我的视图模型中,我已经使用了我在这里的帖子中描述的ViewModel优先方法:在WPF中从ViewModel调用view的最佳实践
当然,在现实世界中,DialogService。ShowDialog必须有更多的选项来配置对话框,例如他们应该执行的按钮和命令。有不同的方式这样做,但它超出了范围:)
我最终混合了Joe White的回答和Adam Mills的回答中的一些代码,因为我需要在编程创建的窗口中显示用户控件。因此,DialogCloser不需要在窗口上,它可以在用户控件本身上
<UserControl ...
xmlns:xw="clr-namespace:Wpf"
xw:DialogCloser.DialogResult="{Binding DialogResult}">
而DialogCloser会找到用户控件的窗口,如果它没有附加到窗口本身。
namespace Wpf
{
public static class DialogCloser
{
public static readonly DependencyProperty DialogResultProperty =
DependencyProperty.RegisterAttached(
"DialogResult",
typeof(bool?),
typeof(DialogCloser),
new PropertyMetadata(DialogResultChanged));
private static void DialogResultChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var window = d.GetWindow();
if (window != null)
window.DialogResult = e.NewValue as bool?;
}
public static void SetDialogResult(DependencyObject target, bool? value)
{
target.SetValue(DialogResultProperty, value);
}
}
public static class Extensions
{
public static Window GetWindow(this DependencyObject sender_)
{
Window window = sender_ as Window;
return window ?? Window.GetWindow( sender_ );
}
}
}