如何在不借助代码隐藏的情况下在用户控件上实现命令?

时间:2022-03-02 19:46:23

I just managed to get my WPF custom message window to work as I intended it... almost:

我只是设法让我的WPF自定义消息窗口按照我预期的方式工作......几乎:

    MessageWindow window;

    public void MessageBox()
    {
        var messageViewModel = new MessageViewModel("Message Title",
            "This message is showing up because of WPF databinding with ViewModel. Yay!",
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec fermentum elit non dui sollicitudin convallis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Integer sed elit magna, non dignissim est. Morbi sed risus id mi pretium facilisis nec non purus. Cras mattis leo sapien. Mauris at erat sapien, vitae commodo turpis. Nam et dui quis mauris mattis volutpat. Donec risus purus, aliquam ut venenatis id, varius vel mauris.");
        var viewModel = new MessageWindowViewModel(messageViewModel, BottomPanelButtons.YesNoCancel);
        window = new MessageWindow(viewModel);
        viewModel.MessageWindowClosing += viewModel_MessageWindowClosing;
        window.ShowDialog();

        var result = viewModel.DialogResult;
        System.Windows.MessageBox.Show(string.Format("result is {0}", result));
    }

    void viewModel_MessageWindowClosing(object sender, EventArgs e)
    {
        window.Close();
    }

Under the hood, there's a "BottomPanel" user control that merely creates a bunch of buttons with their "Visibility" attribute controlled by the MessageWindowViewModel (via property getters such as "IsOkButtonVisible", itself determined by the value of the "BottomPanelButtons" enum passed to the viewmodel's constructor).

在引擎盖下,有一个“BottomPanel”用户控件只能创建一组按钮,其“Visibility”属性由MessageWindowViewModel控制(通过属性getter,如“IsOkButtonVisible”,它本身由传递的“BottomPanelButtons”枚举值决定)到viewmodel的构造函数)。

While this fulfills my requirement of being able to display a message window with collapsible details and a configurable set of buttons at the bottom, I'm disappointed with the way I had to put all the functionality I originally wanted in the BottomPanel control (or rather, into its viewmodel), into the MessageWindowViewModel class:

虽然这满足了我能够在底部显示带有可折叠细节和可配置按钮组的消息窗口的要求,但我对在BottomPanel控件中放置我原本想要的所有功能的方式感到失望(或者更确切地说) ,进入其viewmodel),进入MessageWindowViewModel类:

    public MessageWindowViewModel(MessageViewModel messageViewModel, BottomPanelButtons buttons)
    {
        _messageViewModel = messageViewModel;
        _abortCommand = new DelegateCommand(ExecuteAbortCommand, CanExecuteAbortCommand);
        _applyCommand = new DelegateCommand(ExecuteApplyCommand, CanExecuteApplyCommand);
        _cancelCommand = new DelegateCommand(ExecuteCancelCommand, CanExecuteCancelCommand);
        _closeCommand = new DelegateCommand(ExecuteCloseCommand, CanExecuteCloseCommand);
        _ignoreCommand = new DelegateCommand(ExecuteIgnoreCommand, CanExecuteIgnoreCommand);
        _noCommand = new DelegateCommand(ExecuteNoCommand, CanExecuteNoCommand);
        _okCommand = new DelegateCommand(ExecuteOkCommand, CanExecuteOkCommand);
        _retryCommand = new DelegateCommand(ExecuteRetryCommand, CanExecuteRetryCommand);
        _yesCommand = new DelegateCommand(ExecuteYesCommand, CanExecuteYesCommand);
        Buttons = buttons;
    }

    /// <summary>
    /// Gets/sets a value that determines what buttons appear in the bottom panel.
    /// </summary>
    public BottomPanelButtons Buttons { get; set; }

    public bool IsCloseButtonVisible { get { return Buttons == BottomPanelButtons.ApplyClose || Buttons == BottomPanelButtons.Close; } }
    public bool IsOkButtonVisible { get { return Buttons == BottomPanelButtons.Ok || Buttons == BottomPanelButtons.OkCancel; } }
    public bool IsCancelButtonVisible { get { return Buttons == BottomPanelButtons.OkCancel || Buttons == BottomPanelButtons.RetryCancel || Buttons == BottomPanelButtons.YesNoCancel; } }
    public bool IsYesButtonVisible { get { return Buttons == BottomPanelButtons.YesNo || Buttons == BottomPanelButtons.YesNoCancel; } }
    public bool IsNoButtonVisible { get { return IsYesButtonVisible; } }
    public bool IsApplyButtonVisible { get { return Buttons == BottomPanelButtons.ApplyClose; } }
    public bool IsAbortButtonVisible { get { return Buttons == BottomPanelButtons.AbortRetryIgnore; } }
    public bool IsRetryButtonVisible { get { return Buttons == BottomPanelButtons.AbortRetryIgnore || Buttons == BottomPanelButtons.RetryCancel; } }
    public bool IsIgnoreButtonVisible { get { return Buttons == BottomPanelButtons.AbortRetryIgnore; } }

    public ICommand AbortCommand { get { return _abortCommand; } }
    public ICommand ApplyCommand { get { return _applyCommand; } }
    public ICommand CancelCommand { get { return _cancelCommand; } }
    public ICommand CloseCommand { get { return _closeCommand; } }
    public ICommand IgnoreCommand { get { return _ignoreCommand; } }
    public ICommand NoCommand { get { return _noCommand; } }
    public ICommand OkCommand { get { return _okCommand; } }
    public ICommand RetryCommand { get { return _retryCommand; } }
    public ICommand YesCommand { get { return _yesCommand; } }

    public string AbortButtonText { get { return resx.AbortButtonText; } }
    public string ApplyButtonText { get { return resx.ApplyButtonText; } }
    public string CancelButtonText { get { return resx.CancelButtonText; } }
    public string CloseButtonText { get { return resx.CloseButtonText; } }
    public string IgnoreButtonText { get { return resx.IgnoreButtonText; } }
    public string NoButtonText { get { return resx.NoButtonText; } }
    public string OkButtonText { get { return resx.OkButtonText; } }
    public string RetryButtonText { get { return resx.RetryButtonText; } }
    public string YesButtonText { get { return resx.YesButtonText; } }

    private ICommand _abortCommand; 
    private ICommand _applyCommand; 
    private ICommand _cancelCommand; 
    private ICommand _closeCommand; 
    private ICommand _ignoreCommand; 
    private ICommand _noCommand; 
    private ICommand _okCommand; 
    private ICommand _retryCommand; 
    private ICommand _yesCommand;

And there's even more code below that - the actual Execute and CanExecute handlers, which all do the same thing: set the DialogResult property and raise MessageWindowClosing event:

还有更多的代码 - 实际的Execute和CanExecute处理程序,它们都做同样的事情:设置DialogResult属性并引发MessageWindowClosing事件:

    private void ExecuteCloseCommand(object commandArgs)
    {
        DialogResult = DialogResult.Close;
        if (MessageWindowClosing != null) MessageWindowClosing(this, EventArgs.Empty);
    }

    private bool CanExecuteCloseCommand(object commandArgs)
    {
        return true;
    }

Now this works, but I find it's ugly. I mean, what I'd like to have, is a BottomPanelViewModel class holding all the BottomPanel's functionality. The only thing I like about this, is that I have no code-behind (other than a constructor taking a MessageViewModel in the MessageView class, setting the DataContext property).

现在这个有效,但我发现它很难看。我的意思是,我想拥有的是一个BottomPanelViewModel类,它拥有BottomPanel的所有功能。我唯一喜欢的是,我没有代码隐藏(除了构造函数在MessageView类中使用MessageViewModel,设置DataContext属性)。

So the question is this: is it possible to refactor this code so that I end up with a reusable BottomPanel control, one that embeds its functionality into its own viewmodel and has its own commands? The idea is to have the commands on the BottomPanel control and the handlers in the ViewModel of the containing window... or is that too much of a stretch?

所以问题是:是否有可能重构这些代码,以便最终得到一个可重用的BottomPanel控件,一个将其功能嵌入到自己的viewmodel中并拥有自己的命令的控件?我们的想法是让BottomPanel控件上的命令和包含窗口的ViewModel中的处理程序...或者是太多了?

I've tried many things (dependency properties, static commands, ...), but what I have now is the only way I could manage to get it to work without code-behind. I'm sure there's a better, more focused way of doing things - please excuse my WPF-noobness, this "message box" window is my WPF "Hello World!" first project ever...

我已经尝试过很多东西(依赖属性,静态命令......),但我现在拥有的是唯一可以让它在没有代码隐藏的情况下工作的方法。我确信有更好,更有针对性的做事方式 - 请原谅我的WPF-noobness,这个“消息框”窗口是我的WPF“Hello World!”第一个项目......

2 个解决方案

#1


1  

Based on my own personal experience, I have a few suggestions.

根据我个人的经验,我有一些建议。

First, you can create an interface for any view logic that should be executed by a ViewModel.

首先,您可以为应由ViewModel执行的任何视图逻辑创建接口。

Second, instead of using *ButtonVisibility in the ViewModel, I have found it better to specify a "Mode" of the ViewModel and use a ValueConverter or a Trigger in the view layer to specify what shows in that mode. This makes it to where your ViewModel can't accidentally (through a bug) get into a state that is invalid by giving a scenerio like

其次,我没有在ViewModel中使用* ButtonVisibility,我发现最好指定ViewModel的“Mode”并在视图层中使用ValueConverter或Trigger来指定在该模式下显示的内容。这使得它可以让你的ViewModel不会意外(通过一个bug)进入一个无效的状态,通过给出一个类似的场景

IsYesButtonVisible = true;
IsAbortButtonVisible = true;

I understand that your properties do not have setters, but they could easily be added by someone maintaining code and this is just a simple example.

我知道你的属性没有setter,但是维护代码的人很容易添加它们,这只是一个简单的例子。

For your case here, we really only need the first one.

对于你的情况,我们真的只需要第一个。

Just create an interface that you would like to use. You can rename these to your liking, but here his an example.

只需创建一个您想要使用的界面。你可以根据自己的喜好重命名这些,但这里是他的一个例子。

public interface IDialogService
{
    public void Inform(string message);
    public bool AskYesNoQuestion(string question, string title);
}

Then in your view layer you can create an implementation that is the same across your application

然后在视图层中,您可以在应用程序中创建相同的实现

public class DialogService
{
    public void Inform(string message)
    {
        MessageBox.Show(message);
    }

    public bool AskYesNoQuestion(string question)
    {
        return MessageBox.Show(question, title, MessageBoxButton.YesNo) ==         
                   MessageBoxResult.Yes
    }
}

Then you could use in any ViewModel like this

然后你可以在任何ViewModel中使用这样的

public class FooViewModel
{
    public FooViewModel(IDialogService dialogService)
    {
        DialogService = dialogService;
    }

    public IDialogService DialogService { get; set; }

    public DelegateCommand DeleteBarCommand
    {
        get
        {
            return new DelegateCommand(DeleteBar);
        }
    }

    public void DeleteBar()
    {
        var shouldDelete = DialogService.AskYesNoQuestion("Are you sure you want to delete bar?", "Delete Bar");
        if (shouldDelete)
        {
            Bar.Delete();
        }
    }

    ...
}

#2


0  

I ended up using RoutedCommand, as suggested by @JerKimball. In my searches I've seen dozens of ways to implement this, all probably right but none that left me satisfied.

我最终使用了RoutedCommand,正如@JerKimball所建议的那样。在我的搜索中,我已经看到了很多方法来实现这一点,所有这些都可能正确,但没有一个让我满意。

I'm posting what worked for me as community wiki:

我发布了对社区维基有用的内容:

The BottomPanel control did end up with - minimal - code-behind, because there was no way to bind the CommandBindings to a ViewModel (because commands are not DependencyProperty). So the code-behind merely calls into the "host" ViewModel where the actual implementations of Execute and CanExecute methods reside:

BottomPanel控件确实最终 - 最小 - 代码隐藏,因为没有办法将CommandBindings绑定到ViewModel(因为命令不是DependencyProperty)。因此,代码隐藏仅调用“主机”ViewModel,其中Execute和CanExecute方法的实际实现驻留在:

public partial class BottomPanel : UserControl
{
    public BottomPanel()
    {
        InitializeComponent();
    }

    private void ExecuteOkCommand(object sender, ExecutedRoutedEventArgs e)
    {
        if (DataContext == null) return;
        var viewModel = ((BottomPanelViewModel)DataContext).Host;
        if (viewModel != null) viewModel.ExecuteOkCommand(sender, e);
    }

    private void CanExecuteOkCommand(object sender, CanExecuteRoutedEventArgs e)
    {
        if (DataContext == null) return;
        var viewModel = ((BottomPanelViewModel)DataContext).Host;
        if (viewModel != null) viewModel.CanExecuteOkCommand(sender, e);
    }
    ...
}

In order to avoid tightly coupling the control with a specific ViewModel, I created an interface:

为了避免将控件与特定的ViewModel紧密耦合,我创建了一个接口:

public interface IHasBottomPanel
{
    event EventHandler WindowClosing;
    DialogResult DialogResult { get; set; }
    BottomPanelViewModel BottomPanelViewModel { get; set; }

    void ExecuteOkCommand(object sender, ExecutedRoutedEventArgs e);
    ...

    void CanExecuteOkCommand(object sender, CanExecuteRoutedEventArgs e);
    ...
}

Might be worth noting that the DialogResult I'm using is my own interpretation of it (closer to what WinForms has to offer), because a simple bool just doesn't fulfill the needs - the "Undefined" value is returned when the user "X"'s out of the window:

可能值得注意的是我正在使用的DialogResult是我自己对它的解释(更接近WinForms提供的内容),因为简单的bool只是不能满足需求 - 当用户“返回”未定义“值时X“在窗外:

public enum DialogResult
{
    Undefined,
    Abort,
    Apply,
    Cancel,
    Close,
    Ignore,
    No,
    Ok,
    Retry,
    Yes
}

So, back to the BottomPanel control, in the XAML I could define the command bindings as follows:

所以,回到BottomPanel控件,在XAML中,我可以定义命令绑定,如下所示:

<UserControl.CommandBindings>
    <CommandBinding Command="{x:Static local:BottomPanelViewModel.OkCommand}"
                    Executed="ExecuteOkCommand"
                    CanExecute="CanExecuteOkCommand"/>
    ...

This works because the BottomPanelViewModel class defines the static commands - I could just as well have defined them elsewhere, but they just seem to feel at home right there:

这是有效的,因为BottomPanelViewModel类定义了静态命令 - 我也可以在其他地方定义它们,但它们似乎只是在家里有感觉:

    public static RoutedCommand OkCommand = new RoutedCommand();
    ...

This ViewModel also contains a Host property referred to by the code-behind, which indirectly exposes the ViewModel that will handle the commands:

此ViewModel还包含代码隐藏引用的Host属性,它间接公开将处理命令的ViewModel:

    /// <summary>
    /// Gets the host view model.
    /// </summary>
    public IHasBottomPanel Host { get; private set; }

    /// Gets a value that determines what buttons appear in the bottom panel.
    /// </summary>
    public BottomPanelButtons Buttons { get; private set; }

    /// <summary>
    /// Creates a new ViewModel for a <see cref="BottomPanel"/> control.
    /// </summary>
    /// <param name="buttons">An enum that determines which buttons are shown.</param>
    /// <param name="host">An interface representing the ViewModel that will handle the commands.</param>
    public BottomPanelViewModel(BottomPanelButtons buttons, IHasBottomPanel host)
    {
        Buttons = buttons;
        Host = host;
    }

At this point everything is in place to get things working; I'm using this BottomPanel control on a MessageWindow View, and so the MessageWindowViewModel class implements the IHasBottomPanel interface (the ViewModelBase class merely provides a type-safe way to deal with INotifyPropertyChanged):

在这一点上,一切都已到位,以使事情顺利进行;我在MessageWindow视图上使用这个BottomPanel控件,因此MessageWindowViewModel类实现了IHasBottomPanel接口(ViewModelBase类仅提供了一种处理INotifyPropertyChanged的类型安全方法):

public class MessageWindowViewModel : ViewModelBase, IHasBottomPanel
{
    /// <summary>
    /// Gets/sets ViewModel for the message window's content.
    /// </summary>
    public MessageViewModel ContentViewModel { get { return _messageViewModel; } }
    private MessageViewModel _messageViewModel;

    public MessageWindowViewModel()
        : this(new MessageViewModel())
    { }

    public MessageWindowViewModel(MessageViewModel viewModel)
        : this(viewModel, BottomPanelButtons.Ok)
    { }

    public MessageWindowViewModel(MessageViewModel messageViewModel, BottomPanelButtons buttons)
    {
        _messageViewModel = messageViewModel;
        // "this" is passed as the BottomPanelViewModel's IHasBottomPanel parameter:
        _bottomPanelViewModel = new BottomPanelViewModel(buttons, this);
    }

    ...

    public void ExecuteOkCommand(object sender, ExecutedRoutedEventArgs e)
    {
        DialogResult = DialogResult.Ok;
        if (WindowClosing != null) WindowClosing(this, EventArgs.Empty);
    }

    public void CanExecuteOkCommand(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = _messageViewModel.ShowProgressControls
            ? _messageViewModel.ProgressValue == _messageViewModel.MaxProgressValue
            : true;
    }

So I get what I wanted: the "host" ViewModel controls Execute and CanExecute implementations for all commands in the BottomPanel, and can be implemented differently on another "host". Here there's a way to configure the ViewModel so that the View displays a ProgressBar control, in which case the "Ok" button is only enabled once the ProgressBar's value has reached the maximum value (the "Cancel" button is enabled meanwhile, and gets disabled when "Ok" gets enabled).

所以我得到了我想要的东西:“主机”ViewModel控制BottomPanel中所有命令的Execute和CanExecute实现,并且可以在另一个“主机”上以不同方式实现。这里有一种配置ViewModel的方法,以便View显示ProgressBar控件,在这种情况下,只有ProgressBar的值达到最大值后才启用“Ok”按钮(同时启用“Cancel”按钮,并禁用当“确定”启用时)。

I can then implement my own MsgBox static class and expose various configurations of buttons and icons for various messages displayed to the user:

然后,我可以实现自己的MsgBox静态类,并为显示给用户的各种消息显示各种按钮和图标配置:

public static class MsgBox
{
    private static DialogResult MessageBox(MessageViewModel messageViewModel, BottomPanelButtons buttons)
    {
        var viewModel = new MessageWindowViewModel(messageViewModel, buttons);
        var window = new MessageWindow(viewModel);
        window.ShowDialog();
        return viewModel.DialogResult;
    }

    /// <summary>
    /// Displays an informative message to the user.
    /// </summary>
    /// <param name="title">The message's title.</param>
    /// <param name="message">The message's body.</param>
    /// <returns>Returns <see cref="DialogResult.Ok"/> if user closes the window by clicking the Ok button.</returns>
    public static DialogResult Info(string title, string message)
    {
        return Info(title, message, string.Empty);
    }

    /// <summary>
    /// Displays an informative message to the user.
    /// </summary>
    /// <param name="title">The message's title.</param>
    /// <param name="message">The message's body.</param>
    /// <param name="details">The collapsible message's details.</param>
    /// <returns>Returns <see cref="DialogResult.Ok"/> if user closes the window by clicking the Ok button.</returns>
    public static DialogResult Info(string title, string message, string details)
    {
        var viewModel = new MessageViewModel(title, message, details, MessageIcons.Info);
        return MessageBox(viewModel, BottomPanelButtons.Ok);
    }

    /// <summary>
    /// Displays an error message to the user, with stack trace as message details.
    /// </summary>
    /// <param name="title">The message's title.</param>
    /// <param name="exception">The exception to report.</param>
    /// <returns>Returns <see cref="DialogResult.Ok"/> if user closes the window by clicking the Ok button.</returns>
    public static DialogResult Error(string title, Exception exception)
    {
        var viewModel = new MessageViewModel(title, exception.Message, exception.StackTrace, MessageIcons.Error);
        return MessageBox(viewModel, BottomPanelButtons.Ok);
    }
    ...
}

And this is where @NickFreeman's comment about this question being a possible better fit for CodeReview becomes an undisputable truth: I'd really like to read what the community thinks of this implementation; maybe I've fallen into some pit traps that will bite me later, or maybe I'm violating principles or patterns I'm not aware of.

这就是@ NickFreeman关于这个问题的评论可能更适合CodeReview成为一个无可争议的事实:我真的很想读一下社区对这个实现的看法;也许我陷入了一些陷阱,以后会咬我,或者我可能违反了我不知道的原则或模式。

This question is begging to be migrated!

这个问题乞求迁移!

#1


1  

Based on my own personal experience, I have a few suggestions.

根据我个人的经验,我有一些建议。

First, you can create an interface for any view logic that should be executed by a ViewModel.

首先,您可以为应由ViewModel执行的任何视图逻辑创建接口。

Second, instead of using *ButtonVisibility in the ViewModel, I have found it better to specify a "Mode" of the ViewModel and use a ValueConverter or a Trigger in the view layer to specify what shows in that mode. This makes it to where your ViewModel can't accidentally (through a bug) get into a state that is invalid by giving a scenerio like

其次,我没有在ViewModel中使用* ButtonVisibility,我发现最好指定ViewModel的“Mode”并在视图层中使用ValueConverter或Trigger来指定在该模式下显示的内容。这使得它可以让你的ViewModel不会意外(通过一个bug)进入一个无效的状态,通过给出一个类似的场景

IsYesButtonVisible = true;
IsAbortButtonVisible = true;

I understand that your properties do not have setters, but they could easily be added by someone maintaining code and this is just a simple example.

我知道你的属性没有setter,但是维护代码的人很容易添加它们,这只是一个简单的例子。

For your case here, we really only need the first one.

对于你的情况,我们真的只需要第一个。

Just create an interface that you would like to use. You can rename these to your liking, but here his an example.

只需创建一个您想要使用的界面。你可以根据自己的喜好重命名这些,但这里是他的一个例子。

public interface IDialogService
{
    public void Inform(string message);
    public bool AskYesNoQuestion(string question, string title);
}

Then in your view layer you can create an implementation that is the same across your application

然后在视图层中,您可以在应用程序中创建相同的实现

public class DialogService
{
    public void Inform(string message)
    {
        MessageBox.Show(message);
    }

    public bool AskYesNoQuestion(string question)
    {
        return MessageBox.Show(question, title, MessageBoxButton.YesNo) ==         
                   MessageBoxResult.Yes
    }
}

Then you could use in any ViewModel like this

然后你可以在任何ViewModel中使用这样的

public class FooViewModel
{
    public FooViewModel(IDialogService dialogService)
    {
        DialogService = dialogService;
    }

    public IDialogService DialogService { get; set; }

    public DelegateCommand DeleteBarCommand
    {
        get
        {
            return new DelegateCommand(DeleteBar);
        }
    }

    public void DeleteBar()
    {
        var shouldDelete = DialogService.AskYesNoQuestion("Are you sure you want to delete bar?", "Delete Bar");
        if (shouldDelete)
        {
            Bar.Delete();
        }
    }

    ...
}

#2


0  

I ended up using RoutedCommand, as suggested by @JerKimball. In my searches I've seen dozens of ways to implement this, all probably right but none that left me satisfied.

我最终使用了RoutedCommand,正如@JerKimball所建议的那样。在我的搜索中,我已经看到了很多方法来实现这一点,所有这些都可能正确,但没有一个让我满意。

I'm posting what worked for me as community wiki:

我发布了对社区维基有用的内容:

The BottomPanel control did end up with - minimal - code-behind, because there was no way to bind the CommandBindings to a ViewModel (because commands are not DependencyProperty). So the code-behind merely calls into the "host" ViewModel where the actual implementations of Execute and CanExecute methods reside:

BottomPanel控件确实最终 - 最小 - 代码隐藏,因为没有办法将CommandBindings绑定到ViewModel(因为命令不是DependencyProperty)。因此,代码隐藏仅调用“主机”ViewModel,其中Execute和CanExecute方法的实际实现驻留在:

public partial class BottomPanel : UserControl
{
    public BottomPanel()
    {
        InitializeComponent();
    }

    private void ExecuteOkCommand(object sender, ExecutedRoutedEventArgs e)
    {
        if (DataContext == null) return;
        var viewModel = ((BottomPanelViewModel)DataContext).Host;
        if (viewModel != null) viewModel.ExecuteOkCommand(sender, e);
    }

    private void CanExecuteOkCommand(object sender, CanExecuteRoutedEventArgs e)
    {
        if (DataContext == null) return;
        var viewModel = ((BottomPanelViewModel)DataContext).Host;
        if (viewModel != null) viewModel.CanExecuteOkCommand(sender, e);
    }
    ...
}

In order to avoid tightly coupling the control with a specific ViewModel, I created an interface:

为了避免将控件与特定的ViewModel紧密耦合,我创建了一个接口:

public interface IHasBottomPanel
{
    event EventHandler WindowClosing;
    DialogResult DialogResult { get; set; }
    BottomPanelViewModel BottomPanelViewModel { get; set; }

    void ExecuteOkCommand(object sender, ExecutedRoutedEventArgs e);
    ...

    void CanExecuteOkCommand(object sender, CanExecuteRoutedEventArgs e);
    ...
}

Might be worth noting that the DialogResult I'm using is my own interpretation of it (closer to what WinForms has to offer), because a simple bool just doesn't fulfill the needs - the "Undefined" value is returned when the user "X"'s out of the window:

可能值得注意的是我正在使用的DialogResult是我自己对它的解释(更接近WinForms提供的内容),因为简单的bool只是不能满足需求 - 当用户“返回”未定义“值时X“在窗外:

public enum DialogResult
{
    Undefined,
    Abort,
    Apply,
    Cancel,
    Close,
    Ignore,
    No,
    Ok,
    Retry,
    Yes
}

So, back to the BottomPanel control, in the XAML I could define the command bindings as follows:

所以,回到BottomPanel控件,在XAML中,我可以定义命令绑定,如下所示:

<UserControl.CommandBindings>
    <CommandBinding Command="{x:Static local:BottomPanelViewModel.OkCommand}"
                    Executed="ExecuteOkCommand"
                    CanExecute="CanExecuteOkCommand"/>
    ...

This works because the BottomPanelViewModel class defines the static commands - I could just as well have defined them elsewhere, but they just seem to feel at home right there:

这是有效的,因为BottomPanelViewModel类定义了静态命令 - 我也可以在其他地方定义它们,但它们似乎只是在家里有感觉:

    public static RoutedCommand OkCommand = new RoutedCommand();
    ...

This ViewModel also contains a Host property referred to by the code-behind, which indirectly exposes the ViewModel that will handle the commands:

此ViewModel还包含代码隐藏引用的Host属性,它间接公开将处理命令的ViewModel:

    /// <summary>
    /// Gets the host view model.
    /// </summary>
    public IHasBottomPanel Host { get; private set; }

    /// Gets a value that determines what buttons appear in the bottom panel.
    /// </summary>
    public BottomPanelButtons Buttons { get; private set; }

    /// <summary>
    /// Creates a new ViewModel for a <see cref="BottomPanel"/> control.
    /// </summary>
    /// <param name="buttons">An enum that determines which buttons are shown.</param>
    /// <param name="host">An interface representing the ViewModel that will handle the commands.</param>
    public BottomPanelViewModel(BottomPanelButtons buttons, IHasBottomPanel host)
    {
        Buttons = buttons;
        Host = host;
    }

At this point everything is in place to get things working; I'm using this BottomPanel control on a MessageWindow View, and so the MessageWindowViewModel class implements the IHasBottomPanel interface (the ViewModelBase class merely provides a type-safe way to deal with INotifyPropertyChanged):

在这一点上,一切都已到位,以使事情顺利进行;我在MessageWindow视图上使用这个BottomPanel控件,因此MessageWindowViewModel类实现了IHasBottomPanel接口(ViewModelBase类仅提供了一种处理INotifyPropertyChanged的类型安全方法):

public class MessageWindowViewModel : ViewModelBase, IHasBottomPanel
{
    /// <summary>
    /// Gets/sets ViewModel for the message window's content.
    /// </summary>
    public MessageViewModel ContentViewModel { get { return _messageViewModel; } }
    private MessageViewModel _messageViewModel;

    public MessageWindowViewModel()
        : this(new MessageViewModel())
    { }

    public MessageWindowViewModel(MessageViewModel viewModel)
        : this(viewModel, BottomPanelButtons.Ok)
    { }

    public MessageWindowViewModel(MessageViewModel messageViewModel, BottomPanelButtons buttons)
    {
        _messageViewModel = messageViewModel;
        // "this" is passed as the BottomPanelViewModel's IHasBottomPanel parameter:
        _bottomPanelViewModel = new BottomPanelViewModel(buttons, this);
    }

    ...

    public void ExecuteOkCommand(object sender, ExecutedRoutedEventArgs e)
    {
        DialogResult = DialogResult.Ok;
        if (WindowClosing != null) WindowClosing(this, EventArgs.Empty);
    }

    public void CanExecuteOkCommand(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = _messageViewModel.ShowProgressControls
            ? _messageViewModel.ProgressValue == _messageViewModel.MaxProgressValue
            : true;
    }

So I get what I wanted: the "host" ViewModel controls Execute and CanExecute implementations for all commands in the BottomPanel, and can be implemented differently on another "host". Here there's a way to configure the ViewModel so that the View displays a ProgressBar control, in which case the "Ok" button is only enabled once the ProgressBar's value has reached the maximum value (the "Cancel" button is enabled meanwhile, and gets disabled when "Ok" gets enabled).

所以我得到了我想要的东西:“主机”ViewModel控制BottomPanel中所有命令的Execute和CanExecute实现,并且可以在另一个“主机”上以不同方式实现。这里有一种配置ViewModel的方法,以便View显示ProgressBar控件,在这种情况下,只有ProgressBar的值达到最大值后才启用“Ok”按钮(同时启用“Cancel”按钮,并禁用当“确定”启用时)。

I can then implement my own MsgBox static class and expose various configurations of buttons and icons for various messages displayed to the user:

然后,我可以实现自己的MsgBox静态类,并为显示给用户的各种消息显示各种按钮和图标配置:

public static class MsgBox
{
    private static DialogResult MessageBox(MessageViewModel messageViewModel, BottomPanelButtons buttons)
    {
        var viewModel = new MessageWindowViewModel(messageViewModel, buttons);
        var window = new MessageWindow(viewModel);
        window.ShowDialog();
        return viewModel.DialogResult;
    }

    /// <summary>
    /// Displays an informative message to the user.
    /// </summary>
    /// <param name="title">The message's title.</param>
    /// <param name="message">The message's body.</param>
    /// <returns>Returns <see cref="DialogResult.Ok"/> if user closes the window by clicking the Ok button.</returns>
    public static DialogResult Info(string title, string message)
    {
        return Info(title, message, string.Empty);
    }

    /// <summary>
    /// Displays an informative message to the user.
    /// </summary>
    /// <param name="title">The message's title.</param>
    /// <param name="message">The message's body.</param>
    /// <param name="details">The collapsible message's details.</param>
    /// <returns>Returns <see cref="DialogResult.Ok"/> if user closes the window by clicking the Ok button.</returns>
    public static DialogResult Info(string title, string message, string details)
    {
        var viewModel = new MessageViewModel(title, message, details, MessageIcons.Info);
        return MessageBox(viewModel, BottomPanelButtons.Ok);
    }

    /// <summary>
    /// Displays an error message to the user, with stack trace as message details.
    /// </summary>
    /// <param name="title">The message's title.</param>
    /// <param name="exception">The exception to report.</param>
    /// <returns>Returns <see cref="DialogResult.Ok"/> if user closes the window by clicking the Ok button.</returns>
    public static DialogResult Error(string title, Exception exception)
    {
        var viewModel = new MessageViewModel(title, exception.Message, exception.StackTrace, MessageIcons.Error);
        return MessageBox(viewModel, BottomPanelButtons.Ok);
    }
    ...
}

And this is where @NickFreeman's comment about this question being a possible better fit for CodeReview becomes an undisputable truth: I'd really like to read what the community thinks of this implementation; maybe I've fallen into some pit traps that will bite me later, or maybe I'm violating principles or patterns I'm not aware of.

这就是@ NickFreeman关于这个问题的评论可能更适合CodeReview成为一个无可争议的事实:我真的很想读一下社区对这个实现的看法;也许我陷入了一些陷阱,以后会咬我,或者我可能违反了我不知道的原则或模式。

This question is begging to be migrated!

这个问题乞求迁移!