WPF中MVVM模式下对话框的良好或不良实践是什么?

158

最近我遇到了为我的wpf应用程序创建添加和编辑对话框的问题。

我在代码中想要做的事情就像这样。(我大多使用mvvm的viewmodel first方法)

调用对话框窗口的ViewModel:

var result = this.uiDialogService.ShowDialog("Dialogwindow Title", dialogwindowVM);
// Do anything with the dialog result

它是如何工作的?

首先,我创建了一个对话服务:

public interface IUIWindowDialogService
{
    bool? ShowDialog(string title, object datacontext);
}

public class WpfUIWindowDialogService : IUIWindowDialogService
{
    public bool? ShowDialog(string title, object datacontext)
    {
        var win = new WindowDialog();
        win.Title = title;
        win.DataContext = datacontext;

        return win.ShowDialog();
    }
}

WindowDialog是一个特殊但简单的窗口。我需要它来容纳我的内容:

<Window x:Class="WindowDialog"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    Title="WindowDialog" 
    WindowStyle="SingleBorderWindow" 
    WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight">
    <ContentPresenter x:Name="DialogPresenter" Content="{Binding .}">

    </ContentPresenter>
</Window>

在wpf中对话框的一个问题是dialogresult = true只能在代码中实现。这就是为什么我创建了一个接口让我的dialogviewmodel去实现它。

public class RequestCloseDialogEventArgs : EventArgs
{
    public bool DialogResult { get; set; }
    public RequestCloseDialogEventArgs(bool dialogresult)
    {
        this.DialogResult = dialogresult;
    }
}

public interface IDialogResultVMHelper
{
    event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
}

每当我的ViewModel认为是时候设置dialogresult = true时,就会触发此事件。
public partial class DialogWindow : Window
{
    // Note: If the window is closed, it has no DialogResult
    private bool _isClosed = false;

    public DialogWindow()
    {
        InitializeComponent();
        this.DialogPresenter.DataContextChanged += DialogPresenterDataContextChanged;
        this.Closed += DialogWindowClosed;
    }

    void DialogWindowClosed(object sender, EventArgs e)
    {
        this._isClosed = true;
    }

    private void DialogPresenterDataContextChanged(object sender,
                              DependencyPropertyChangedEventArgs e)
    {
        var d = e.NewValue as IDialogResultVMHelper;

        if (d == null)
            return;

        d.RequestCloseDialog += new EventHandler<RequestCloseDialogEventArgs>
                                    (DialogResultTrueEvent).MakeWeak(
                                        eh => d.RequestCloseDialog -= eh;);
    }

    private void DialogResultTrueEvent(object sender, 
                              RequestCloseDialogEventArgs eventargs)
    {
        // Important: Do not set DialogResult for a closed window
        // GC clears windows anyways and with MakeWeak it
        // closes out with IDialogResultVMHelper
        if(_isClosed) return;

        this.DialogResult = eventargs.DialogResult;
    }
 }

现在至少我必须在我的资源文件(app.xaml或其他文件)中创建一个DataTemplate:
<DataTemplate DataType="{x:Type DialogViewModel:EditOrNewAuswahlItemVM}" >
        <DialogView:EditOrNewAuswahlItem/>
</DataTemplate>

好的,这就是全部了,现在我可以从我的视图模型中调用对话框:

 var result = this.uiDialogService.ShowDialog("Dialogwindow Title", dialogwindowVM);

现在我的问题是,你觉得这个解决方案有什么问题吗? 编辑:为了完整性。ViewModel 应该实现 IDialogResultVMHelper 接口,然后可以在 OkCommand 中引发它或类似的东西。
public class MyViewmodel : IDialogResultVMHelper
{
    private readonly Lazy<DelegateCommand> _okCommand;

    public MyViewmodel()
    {
         this._okCommand = new Lazy<DelegateCommand>(() => 
             new DelegateCommand(() => 
                 InvokeRequestCloseDialog(
                     new RequestCloseDialogEventArgs(true)), () => 
                         YourConditionsGoesHere = true));
    }

    public ICommand OkCommand
    { 
        get { return this._okCommand.Value; } 
    }

    public event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
    private void InvokeRequestCloseDialog(RequestCloseDialogEventArgs e)
    {
        var handler = RequestCloseDialog;
        if (handler != null) 
            handler(this, e);
    }
 }

编辑2:我使用了这里的代码来使我的事件处理程序注册为弱引用:
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(该网站已不再存在,WebArchive镜像)

public delegate void UnregisterCallback<TE>(EventHandler<TE> eventHandler) 
    where TE : EventArgs;

public interface IWeakEventHandler<TE> 
    where TE : EventArgs
{
    EventHandler<TE> Handler { get; }
}

public class WeakEventHandler<T, TE> : IWeakEventHandler<TE> 
    where T : class 
    where TE : EventArgs
{
    private delegate void OpenEventHandler(T @this, object sender, TE e);

    private readonly WeakReference mTargetRef;
    private readonly OpenEventHandler mOpenHandler;
    private readonly EventHandler<TE> mHandler;
    private UnregisterCallback<TE> mUnregister;

    public WeakEventHandler(EventHandler<TE> eventHandler,
                                UnregisterCallback<TE> unregister)
    {
        mTargetRef = new WeakReference(eventHandler.Target);

        mOpenHandler = (OpenEventHandler)Delegate.CreateDelegate(
                           typeof(OpenEventHandler),null, eventHandler.Method);

        mHandler = Invoke;
        mUnregister = unregister;
    }

    public void Invoke(object sender, TE e)
    {
        T target = (T)mTargetRef.Target;

        if (target != null)
            mOpenHandler.Invoke(target, sender, e);
        else if (mUnregister != null)
        {
            mUnregister(mHandler);
            mUnregister = null;
        }
    }

    public EventHandler<TE> Handler
    {
        get { return mHandler; }
    }

    public static implicit operator EventHandler<TE>(WeakEventHandler<T, TE> weh)
    {
        return weh.mHandler;
    }
}

public static class EventHandlerUtils
{
    public static EventHandler<TE> MakeWeak<TE>(this EventHandler<TE> eventHandler, 
                                                    UnregisterCallback<TE> unregister)
        where TE : EventArgs
    {
        if (eventHandler == null)
            throw new ArgumentNullException("eventHandler");

        if (eventHandler.Method.IsStatic || eventHandler.Target == null)
            throw new ArgumentException("Only instance methods are supported.",
                                            "eventHandler");

        var wehType = typeof(WeakEventHandler<,>).MakeGenericType(
                          eventHandler.Method.DeclaringType, typeof(TE));

        var wehConstructor = wehType.GetConstructor(new Type[] 
                             { 
                                 typeof(EventHandler<TE>), typeof(UnregisterCallback<TE>) 
                             });

        IWeakEventHandler<TE> weh = (IWeakEventHandler<TE>)wehConstructor.Invoke(
                                        new object[] { eventHandler, unregister });

        return weh.Handler;
    }
}

1
你可能在 WindowDialog 的 XAML 中缺少 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 引用。 - Adiel Yaacov
实际上,命名空间是xmlns:x="[http://]schemas.microsoft.com/winfx/2006/xaml",不包括括号。 - reggaeguitar
嗨@blindmeis。我知道已经过去了将近5年,但我卡在关闭对话框部分了。你能否请解释一下那部分? - Adil Mammadov
2
嗨!我来晚了。我不明白你的Service如何引用WindowDialog。你的模型层次结构是怎样的?在我看来,View持有对Viewmodel程序集的引用,而Viewmodel则持有对Service和Model程序集的引用。因此,Service层不应该知道WindowDialog视图。我错过了什么吗? - Moe45673
3
嗨@blindmeis,我正在努力理解这个概念,不知道是否有一些在线示例项目可以供我查看?有很多事情让我感到困惑。 - Hank
显示剩余9条评论
3个回答

50

这是一个不错的方法,我过去也使用过类似的方法。继续前进吧!

我会做一件小事,就是使事件接收到一个布尔值,以便在需要将DialogResult设置为“false”时使用。

event EventHandler<RequestCloseEventArgs> RequestCloseDialog;

以及 EventArgs 类:

public class RequestCloseEventArgs : EventArgs
{
    public RequestCloseEventArgs(bool dialogResult)
    {
        this.DialogResult = dialogResult;
    }

    public bool DialogResult { get; private set; }
}

如果不使用服务,而是使用一种回调来促进ViewModel和View之间的交互会怎样呢?例如,View在ViewModel中执行一个命令,然后当所有操作完成时,ViewModel触发一个回调,以便View显示命令的结果。我仍然无法让我的团队接受在ViewModel中处理对话框交互的服务。 - Matthew S

16

我已经使用了一个几乎相同的方法已有数个月,现在感到非常满意(也就是说,我还没有完全重写它的冲动...)

在我的实现中,我使用了一个 IDialogViewModel,它公开了诸如标题、要显示的标准按钮(以便在所有对话框中具有一致的外观)、RequestClose 事件和其他一些内容,以便能够控制窗口大小和行为。


谢谢,标题应该放在我的IDialogViewModel中。其他属性,如大小、标准按钮,我会留下来,因为这些都来自数据模板。 - blindmeis
1
这也是我一开始做的事情,只需使用SizeToContent来控制窗口的大小。但在某些情况下,我需要使窗口可调整大小,因此我必须稍微调整一下... - Thomas Levesque
@ThomasLevesque,您的ViewModel中包含的按钮,它们是实际的UI Button对象还是代表按钮的对象? - Thomas
3
@Thomas,这里指代表示按钮的对象。在 ViewModel 中,您不应该引用任何 UI 对象,但需保持原意不变,使语言更加通俗易懂。 - Thomas Levesque

5
如果您说的是对话框而不仅仅是弹出消息框,请考虑下面我的方法。关键点如下:
  1. 我把对Module Controller的引用传递给每个ViewModel的构造函数中(您可以使用注入)。
  2. Module Controller具有创建对话框窗口的公共/内部方法(仅创建,不返回结果)。因此,在ViewModel中打开对话框窗口时,我编写: controller.OpenDialogEntity(bla, bla...)
  3. 每个对话框窗口通过 弱事件 通知其结果(例如 OK Save Cancel 等)。如果您使用PRISM,则使用此EventAggregator更容易发布通知。
  4. 为了处理对话框结果,我使用订阅通知(再次使用弱事件 EventAggregator 在PRISM的情况下)。为了减少对此类通知的依赖性,请使用具有标准通知的独立类。

优点:

  • 代码更少。我不介意使用接口,但我见过太多项目过度使用接口和抽象层导致更多的麻烦而不是帮助。
  • 通过Module Controller打开对话框窗口是避免强引用的简单方法,并且仍然允许使用模拟测试。
  • 通过弱事件进行通知减少了潜在内存泄漏的数量。

缺点:

  • 在处理程序中很难区分所需的通知和其他通知。两种解决方案:
    • 在打开对话框窗口时发送唯一令牌,并在订阅中检查该令牌
    • 使用通用通知类<T>,其中T是实体的枚举(或为简单起见,它可以是ViewModel的类型)。
  • 对于项目应该达成关于使用通知类的协议以防止重复。
  • 对于非常大的项目,Module Controller可能会被创建窗口的方法压垮。在这种情况下,最好将其分成几个模块。

P.S.我已经使用这种方法很长时间了,并准备通过评论来捍卫它的合法性并提供一些示例(如果需要)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接