Хорошая или плохая практика для диалогов в wpf с MVVM?

148

В последнее время у меня возникла проблема создания диалогов добавления и редактирования для моего wpf-приложения.

Все, что я хочу сделать в своем коде, было примерно таким. (Я в основном использую viewmodel первый подход с mvvm)

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

Теперь, по крайней мере, я должен создать DataTemplateв своем файле ресурсов ( app.xamlили что-то):

<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: я использовал код отсюда, чтобы сделать мой EventHandler регистр слабым:
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(веб-сайт больше не существует, WebArchive Mirror )

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;
    }
}
blindmeis
источник
1
вам, вероятно, не хватает ссылки xmlns: x = " schemas.microsoft.com/winfx/2006/xaml " в XAML WindowDialog.
Адиэль Яаков
На самом деле пространство имен - это xmlns: x = "[http: //] schemas.microsoft.com/winfx/2006/xaml" без скобок
reggaeguitar
1
Здравствуй! Опоздавший здесь. Я не понимаю, как ваша служба имеет ссылку на WindowDialog. Какова иерархия ваших моделей? На мой взгляд, представление содержит ссылку на сборку Viewmodel и Viewmodel на сборки Service и Model. Таким образом, сервисный уровень не будет знать представления WindowDialog. Чего мне не хватает?
Moe45673
2
Привет @blindmeis, просто пытаюсь обернуть голову вокруг этой концепции, я не думаю, что есть какой-нибудь пример проекта, который я могу выбрать? Есть много вещей, которые меня смущают.
Хэнк

Ответы:

48

Это хороший подход, и я использовал подобные в прошлом. Действуй!

Одна небольшая вещь, которую я определенно сделаю, это заставит событие получать логическое значение, когда вам нужно установить «false» в DialogResult.

event EventHandler<RequestCloseEventArgs> RequestCloseDialog;

и класс EventArgs:

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

    public bool DialogResult { get; private set; }
}
Джулиан Домингес
источник
Что если вместо использования сервисов использовать своего рода Callback для облегчения взаимодействия с ViewModel и View? Например, View выполняет команду в ViewModel, затем, когда все сказано и сделано, ViewModel запускает функцию обратного вызова для View, чтобы отобразить результаты команды. Я все еще не могу заставить свою команду работать с использованием Служб для обработки диалоговых взаимодействий во ViewModel.
Мэтью С.
15

Уже несколько месяцев я использую почти идентичный подход, и я очень доволен им (т.е. я еще не чувствовал желания полностью переписать его ...)

В моей реализации я использую объект, IDialogViewModelкоторый отображает такие вещи, как заголовок, стандартные кнопки для показа (чтобы иметь одинаковый вид во всех диалогах), RequestCloseсобытие и некоторые другие вещи, чтобы иметь возможность контролировать размер окна и поведение

Томас Левеск
источник
THX, заголовок должен действительно идти в моей IDialogViewModel. другие свойства, такие как размер, стандартная кнопка, которую я оставлю, потому что все это, по крайней мере, исходит из таблицы данных.
вслепую
1
Это то, что я сделал сначала, просто используйте SizeToContent для контроля размера окна. Но в одном случае мне нужно было изменить размер окна, поэтому мне пришлось немного его настроить ...
Томас Левеск,
@ThomasLevesque кнопки, содержащиеся в вашей ViewModel, они на самом деле объекты кнопок пользовательского интерфейса или объекты, представляющие кнопки?
Томас
3
@Tommas, объекты, представляющие кнопки. Вы никогда не должны ссылаться на объекты пользовательского интерфейса в ViewModel.
Томас Левеск
2

Если вы говорите об диалоговых окнах, а не только о всплывающих окнах сообщений, рассмотрите мой подход ниже. Ключевые моменты:

  1. Я передаю ссылку Module Controllerв конструктор каждого ViewModel(вы можете использовать инъекцию).
  2. Это Module Controllerимеет публичные / внутренние методы для создания диалоговых окон (просто создание, без возврата результата). Следовательно, чтобы открыть диалоговое окно в ViewModelя пишу:controller.OpenDialogEntity(bla, bla...)
  3. Каждое диалоговое окно уведомляет о своем результате (например, OK , Сохранить , Отмена и т. Д.) Через Слабые события . Если вы используете PRISM, то легче публиковать уведомления с помощью этого EventAggregator .
  4. Для обработки результатов диалога я использую подписку на уведомления (снова Weak Events и EventAggregator в случае PRISM). Чтобы уменьшить зависимость от таких уведомлений, используйте независимые классы со стандартными уведомлениями.

Плюсы:

  • Меньше кода. Я не против использования интерфейсов, но я видел слишком много проектов, в которых чрезмерное использование интерфейсов и уровней абстракции вызывает больше проблем, чем помощи.
  • Открытые диалоговые окна Module Controller- это простой способ избежать сильных ссылок и все же позволяет использовать макеты для тестирования.
  • Уведомление через слабые события уменьшает количество потенциальных утечек памяти.

Минусы:

  • Не легко отличить требуемое уведомление от других в обработчике. Два решения:
    • отправьте уникальный токен при открытии диалогового окна и проверьте этот токен в подписке
    • используйте общие классы уведомлений, <T>где Tперечисление сущностей (или для простоты это может быть тип ViewModel).
  • Для проекта должно быть соглашение об использовании классов уведомлений для предотвращения их дублирования.
  • Для очень больших проектов это Module Controllerможет быть перегружено методами создания окон. В этом случае лучше разбить его на несколько модулей.

PS Я уже давно использую этот подход и готов отстаивать его соответствие в комментариях и приводить некоторые примеры, если это необходимо.

Алекс Клаус
источник