0

我只是设法让我的 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 控制(通过诸如“IsOkButtonVisible”之类的属性获取器,它本身由传递的“BottomPanelButtons”枚举的值决定到视图模型的构造函数)。

虽然这满足了我能够在底部显示带有可折叠详细信息和一组可配置按钮的消息窗口的要求,但我对必须将我最初想要的所有功能放入 BottomPanel 控件(或者更确切地说,进入其视图模型),进入 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属性并引发MessageWindowClosing事件:

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

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

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

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

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

4

2 回答 2

1

根据我个人的经验,我有几点建议。

首先,您可以为任何应该由ViewModel.

ViewModel其次,我发现最好指定 的“模式”并在视图层中ViewModel使用 aValueConverter或 a来指定在该模式下显示的内容,而不是在 中使用 *ButtonVisibility 。Trigger这使得您的 ViewModel 不会意外地(通过错误)进入一个无效的状态,通过提供类似的场景

IsYesButtonVisible = true;
IsAbortButtonVisible = true;

我了解您的属性没有设置器,但维护代码的人可以轻松添加它们,这只是一个简单的示例。

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

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

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

    ...
}
于 2013-04-04T01:41:11.120 回答
0

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

我正在发布对我有用的社区 wiki:

BottomPanel控件确实以 - 最小 - 代码隐藏结束,因为没有办法将 绑定CommandBindings到 ViewModel (因为命令不是DependencyProperty)。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);
    }
    ...
}

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

在这一点上,一切都已准备就绪,可以正常工作了;我在MessageWindowView 上使用了这个 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;
    }

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

然后我可以实现自己的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 的评论成为无可争辩的事实的地方:我真的很想了解社区对这个实现的看法;也许我落入了一些以后会咬我的陷阱,或者我违反了我不知道的原则或模式。

这个问题求迁移!

于 2013-04-05T02:09:25.870 回答