我有一个基于MVVM-Light的WPF应用程序,带有一个对话框服务(称为WindowManager
),它打开绑定到预先启动的对话框视图模型的对话框窗口,如下所示:
private enum ViewModelKind
{
PlanningGridVM,
InputDialogVM,
TreeViewDialogVM,
SaveFileDialogVM,
MessageBoxVM
}
/// <summary>
/// Shows the Window linked to this ViewModel as a dialog window.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model.</typeparam>
/// <returns>Tri-state boolean dialog response.</returns>
public bool? ShowDialog<TViewModel>(string key = null)
{
ViewModelKind name;
// Attempt to parse the type-parameter to enum
Enum.TryParse(typeof(TViewModel).Name, out name);
Window view = null;
switch (name)
{
// removed some irrelevant cases...
case ViewModelKind.InputDialogVM:
view = new InputDialogView();
System.Diagnostics.Debug.WriteLine(
view.GetHashCode(), "New Window HashCode");
view.Height = 200;
result = view.ShowDialog();
default:
return true;
}
}
对话框的XAML是这样开始的:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:b="clr-namespace:MyCompany.Common.Behaviours"
x:Class="MyCompany.Common.Views.InputDialogView" mc:Ignorable="d"
DataContext="{Binding InputDialogVM, Source={StaticResource Locator}}"
Title="{Binding DisplayName}" MinHeight="200" MinWidth="300" MaxHeight="200"
b:WindowBehaviours.DialogResult="{Binding DialogResult}"
WindowStyle="ToolWindow" ShowInTaskbar="False"
WindowStartupLocation="CenterScreen"
Height="200" Width="300">
视图模型在其构造函数中适当地向Messenger注册,它们通过重置视图模型属性来响应初始化消息。
为了正确关闭我的"确定/取消"对话框,我有一个名为DialogResult
的附加属性,它也按预期工作…
/// <summary>
/// DialogResult
/// </summary>
public static readonly DependencyProperty DialogResultProperty = DependencyProperty
.RegisterAttached(
"DialogResult",
typeof(bool?),
typeof(WindowBehaviours),
new PropertyMetadata(null, DialogResultChanged));
public static void SetDialogResult(Window target, bool? value)
{
target.SetValue(DialogResultProperty, value);
}
private static void DialogResultChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var window = obj as Window;
System.Diagnostics.Debug.WriteLine(
window.GetHashCode(), "Attempting to update DialogResult on Hashcode");
if (window != null && window.IsActive)
{
window.DialogResult = e.NewValue as bool?;
}
}
…但有一点需要注意。你注意到Debug输出我添加跟踪窗口实例的HashCode吗?它为我确认了以下内容:
当你有一个可重用的视图模型实例,由对话框视图通过XAML中的DataContext绑定访问,并且你顺序地打开一个新对话框几次,这些对话框实例保持打开,即使在它们的OnClosed事件被引发之后,即使对话框不再可见!
这样做的最终效果是,我必须检查窗口的IsActive
属性,同时检查窗口是否为null。如果我不这样做,系统将尝试在仍然存在的每个对话框幻影上设置window.DialogResult
,导致System.InvalidOperationException
异常:"只有在创建窗口并显示为对话框后才能设置dialgresult"。
调试输出
New Window HashCode: 4378943
Attempting to update DialogResult on Hashcode: 4378943
New Window HashCode: 53142588
Attempting to update DialogResult on Hashcode: 53142588
New Window HashCode: 47653507
Attempting to update DialogResult on Hashcode: 53142588
Attempting to update DialogResult on Hashcode: 47653507
New Window HashCode: 57770831
Attempting to update DialogResult on Hashcode: 53142588
Attempting to update DialogResult on Hashcode: 57770831
New Window HashCode: 49455573
Attempting to update DialogResult on Hashcode: 53142588
Attempting to update DialogResult on Hashcode: 57770831
Attempting to update DialogResult on Hashcode: 49455573
New Window HashCode: 20133242
Attempting to update DialogResult on Hashcode: 53142588
Attempting to update DialogResult on Hashcode: 57770831
Attempting to update DialogResult on Hashcode: 49455573
Attempting to update DialogResult on Hashcode: 20133242
很多时候,我看到它说附加行为存储特定于实例的属性值。
现在很清楚,那些过期的对话框仍然注册到单个视图模型实例的INPC事件。如何确保关闭对话框从INPC事件中未注册?
感谢Kess为我指出了正确的方向…问题不在于销毁对话框实例,而在于确保每个对话框实例都有一个唯一的视图模型实例!然后,您只需要使用内置的Cleanup()
方法从Messenger注销视图模型。
技巧是使用ServiceLocator
的GetInstance<T>(string key)
方法,并将该密钥传递给WindowManager
。
<标题> 解决方案
InputDialogView XAML:
删除将DataContext分配给ViewModelLocator属性的行
DataContext="{Binding InputDialogVM, Source={StaticResource Locator}}"
WindowManager:
使用传入ShowDialog的字符串键,并使用ServiceLocator(带键)来获取唯一的视图模型,并显式地设置视图。DataContext
public bool? ShowDialog<TViewModel>(string key = null)
{
ViewModelKind name;
Enum.TryParse(typeof(TViewModel).Name, out name);
Window view = null;
switch (name)
{
case ViewModelKind.InputDialogVM:
view = new InputDialogView();
// Added this line
view.DataContext = ServiceLocator.Current
.GetInstance<InputDialogVM>(key);
view.Height = 200;
result = view.ShowDialog();
default:
return true;
}
}
MainViewModel:
使用唯一的字符串键
创建ServiceLocator
的新视图模型internal void ExecuteChangeState()
{
string key = Guid.NewGuid().ToString();
InputDialogVM viewModel = ServiceLocator.Current
.GetInstance<InputDialogVM>(key);
// Use a nested tuple class here to send initialization parameters
var inputDialogParams = new InputDialogVM.InitParams(
"Please provide a reason", "Deactivation Prompt", true);
// This message sends some critical values to all registered VM instances
Messenger.Default.Send(new NotificationMessage<InputDialogVM.InitParams>(
inputDialogParams, CommonMsg.InitInputDialog));
if (_windowManager.ShowDialog<InputDialogVM>(key) == true)
{
var logEntry = new DealsLogEntry()
{
LogDateTime = DateTime.Now,
LogUsername = this.CurrentUser.Username,
Reason = viewModel.InputString
};
_logRepository.Create(logEntry);
}
// Unregister this instance from the Messenger class
viewModel.Cleanup();
}
标题>