2013-04-03 1 views
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», который просто создает кучу кнопок с их Атрибут «Видимость», управляемый MessageWindowViewModel (через атрибуты свойств, такие как «IsOkButtonVisible», сам определяется значением перечисления «BottomPanelButtons», переданным конструктору 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; 

и есть еще код ниже - фактические 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; 
    } 

Теперь это работает, но я нахожу его уродливым. Я имею в виду, что я хотел бы иметь, это класс BottomPanelViewModel, содержащий все функции BottomPanel. Единственное, что мне нравится в этом, это то, что у меня нет кода (кроме конструктора, который принимает MessageViewModel в классе MessageView, устанавливая свойство DataContext).

Итак, вопрос заключается в следующем: можно ли реорганизовать этот код, чтобы я мог использовать многократно используемый элемент управления BottomPanel, который встраивает его функциональность в свою собственную модель просмотра и имеет свои собственные команды? Идея состоит в том, чтобы иметь команды в элементе управления BottomPanel и обработчики в ViewModel содержащего окна ... или это слишком много растягивается?

Я пробовал много вещей (свойства зависимостей, статические команды, ...), но теперь у меня есть единственный способ заставить его работать без кода. Я уверен, что есть лучший, более сфокусированный способ делать вещи - пожалуйста, извините мой WPF-noobness, это окно «окно сообщения» - это мой WPF «Hello World!». первый проект когда-либо ...

+0

Вы считаете, что вы ищете термин «RoutedCommand»; эта ссылка может помочь: http://joshsmithonwpf.wordpress.com/2008/03/18/understanding-routed-commands/ – JerKimball

+0

Если вы помечаете свое сообщение как 'C#', вы получите подсветку синтаксиса, и вы, вероятно, получите больше ответов. –

+0

Кроме того, этот вопрос может быть лучше подходит для [Code Review] (http://codereview.stackexchange.com/) –

ответ

1

Основываясь на моем собственном опыте, у меня есть несколько предложений.

Во-первых, вы можете создать интерфейс для любой логики вида, который должен быть выполнен с помощью ViewModel.

Во-вторых, вместо того, чтобы использовать * ButtonVisibility в ViewModel, я обнаружил, что лучше указать «Mode» в ViewModel и использовать ValueConverter или Trigger в вид слоя, чтобы указать, что показывает в этом режиме. Это делает его туда, где ваш ViewModel не может случайно (через жук) попадет в состояние, которое является недопустимым, давая scenerio как

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

    ... 
} 
+0

Интересный ... Этот конвертер может принимать значение перечисления 'Buttons' и потенциально заменять логику прямого связывания (хотя модульные тесты могут легко защитить от недопустимого состояния). Но это не касается моей проблемы с функциональностью BottomPanel, которая написана в модели представления MessageWindow ... и теперь я просто понял, что могу передать модель представления BottomPanel интерфейсом, представляющим его модель представления «родительского» окна (а не сама «родительская» модель просмотра) чтобы держать вещи слабо связанными ... что-то вроде «IHasBottomPanel», разоблачая нужные обработчики. Имеет ли это смысл? –

+0

Обновление: путь 'IHasBottomPanel' работает! Поэтому я переместил всю спецификацию BottomPanel в класс «BottomPanelViewModel» и зарегистрировал 'DelegateCommand' следующим образом:' _okCommand = new DelegateCommand (Host.ExecuteOkCommand, Host.CanExecuteOkCommand); 'где' Host' является реализацией 'IHasBottomPanel' , и это сработало! Я буду исследовать предложение @ JerKimball использовать 'RoutedCommand' вместо этого, но на данный момент у меня есть код, написанный там, где он принадлежит. Я серьезно рассмотрю предложение конвертера рефакторинга: +1. Что касается диалогового интерфейса, я думаю, что в этом случае я предпочел бы подражать «MsgBox». –

0

Я кончался используя RoutedCommand, как было предложено @JerKimball. В моих поисках я видел десятки способов реализовать это, все, вероятно, правильно, но никто не оставил меня удовлетворенным.

Я отправляю, что работал для меня, как сообщества вики:

управления 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); 
    } 
    ... 
} 

Для того, чтобы избежать плотно соединения управления с конкретным 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 свойство REFE rred к по фоновому коду, который косвенно выставляет 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; 
    } 

На данный момент все находится в месте, чтобы заставить его работать; Я использую этот контроль BottomPanel на MessageWindow View, и поэтому 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; 
    } 

Так я получаю то, что я хотел: «Host» ViewModel управляет Execute и CanExecute реализациями для всех команд в BottomPanel и может быть реализован по-разному на другом «хосте». Здесь есть способ настроить ViewModel так, чтобы View отображал элемент управления ProgressBar, и в этом случае кнопка «Ok» активируется только после того, как значение ProgressBar достигло максимального значения (кнопка «Отмена» включена тем временем и отключается когда активируется «Ok»).

Затем я могу реализовать свой собственный 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, становится неоспоримой истиной: мне бы очень хотелось узнать, что сообщество думает об этой реализации; возможно, я попал в некоторые ловушки ямы, которые укусят меня позже, или, может быть, я нарушаю принципы или узоры, о которых я не знаю.

Этот вопрос просят мигрировать!

 Смежные вопросы

  • Нет связанных вопросов^_^