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

时间:2013-04-03 21:33:55

标签: c# wpf mvvm user-controls icommand

我设法让我的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();
    }

在引擎盖下,有一个" BottomPanel"用户控件只会创建一堆按钮,其中包含" Visibility"由MessageWindowViewModel控制的属性(通过属性getter,例如" IsOkButtonVisible",它本身由" BottomPanelButtons" enum传递给viewmodel的构造函数)的值确定。

虽然这满足了我能够在底部显示带有可折叠细节和可配置按钮组的消息窗口的要求,但我对我必须放置我最初想要的所有功能的方式感到失望。 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;

还有更多代码 - 实际的ExecuteCanExecute处理程序,它们都做同样的事情:设置DialogResult属性并引发{{1 event:

MessageWindowClosing

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

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

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

2 个答案:

答案 0 :(得分:1)

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

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

其次,我没有在ViewModel中使用* ButtonVisibility,而是发现更好地指定&#34;模式&#34; ViewModel的{​​{1}}并在视图图层中使用ValueConverterTrigger来指定在该模式下显示的内容。这使得ViewModel无法通过提供像

这样的场景而无意中(通过错误)进入无效状态
IsYesButtonVisible = true;
IsAbortButtonVisible = true;

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

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

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

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

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

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

然后你可以在任何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();
        }
    }

    ...
}

答案 1 :(得分:0)

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

我发布了适用于社区wiki的内容:

BottomPanel控件确实以 - 最小 - 代码隐藏结束,因为无法将CommandBindings绑定到ViewModel(因为命令不是DependencyProperty)。因此,代码隐藏仅调用“宿主”ViewModel,ExecuteCanExecute方法的实际实现位于其中:

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

为了避免将控件与特定的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);
    ...
}

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

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

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

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

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

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

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

此时一切都已准备就绪;我在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;
    }

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

然后,我可以实现自己的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);
    }
    ...
}

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

这个问题乞求迁移!