How can I ensure closed dialogs are unregistered from INPC events?

ε祈祈猫儿з 提交于 2019-12-24 11:56:55

问题


I have an MVVM-Light based WPF application, with a dialog service (called WindowManager) that opens up dialog windows bound to pre-initiated dialog view-models, like this:

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;
    }
}

The dialog's XAML starts with this:

<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">

The view-models appropriately register with Messenger in their constructors, and they respond to initialization messages by resetting the view-model properties. This all works as intended.

In order to properly close my "Okay/Cancel" dialogs, I have an attached property called DialogResult, which also works as expected...

/// <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?;
    }
}

...but for one caveat. Did you notice that Debug output I added to track the window instance's HashCode? It confirmed the following for me:

When you have one reusable view-model instance, accessed by the dialog view through a DataContext binding in the XAML, and you sequentially open up a new dialog several times, those dialog instances remain open, even after their OnClosed event has been raised, and even though the dialog is not visible anymore!

The nett effect of this is that I have to check the window's IsActive property in conjunction with checking the window for null. If I don't, the system will try to set window.DialogResult on every dialog phantom that still remains, resulting in a System.InvalidOperationException exception: "DialogResult can be set only after Window is created and shown as dialog".


Debug Output

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

Question

Many times, I have seen it said that an attached behaviour stores the value of the property specific to an instance. Why is this behaving the opposite way?

It is clear now that those expired dialogs are still registered to the single view-model instance's INPC events. How can I ensure closed dialogs are unregistered from INPC events?


回答1:


Thanks to Kess for pointing me in the right direction... the problem was never about destroying a dialog instance, but rather to make sure you have a unique view-model instance for each dialog instance! Then you only have to unregister the view-model from Messenger with the built-in Cleanup() method.

The trick is to use method, GetInstance<T>(string key) of ServiceLocator and to pass that key into your WindowManager.


Solution

InputDialogView XAML:

Remove the line that assigns DataContext to a ViewModelLocator property

DataContext="{Binding InputDialogVM, Source={StaticResource Locator}}"

WindowManager:

Use the string key passed into ShowDialog and use ServiceLocator (with the key) to fetch the unique view-model, and explicitly set view.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:

Create a new view-model with ServiceLocator using a unique string key

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();
}


来源:https://stackoverflow.com/questions/24040033/how-can-i-ensure-closed-dialogs-are-unregistered-from-inpc-events

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!