Хороша чи погана практика для діалогів у 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);
    }
 }

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

1
ви, ймовірно, не вистачаєте у програмі WindowDialog XAML референс xmlns: x = " schemas.microsoft.com/winfx/2006/xaml ".
Адіель Яаков

Насправді простір імен xmlns: x = "[http: //] schemas.microsoft.com/winfx/2006/xaml" без дужок
reggaeguitar


1
Привіт! Пізній тут. Я не розумію, як ваша служба має посилання на WindowDialog. Яка ієрархія ваших моделей? На мій погляд, View містить посилання на збірку Viewmodel, а Viewmodel на збори служби та моделі. Тим самим, рівень Сервісу не знає про подання 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; }
}

Що робити, якщо замість використання сервісів використовується якийсь зворотний виклик, щоб полегшити взаємодію з ViewModel та Переглядом? Наприклад, View виконує команду в ViewModel, тоді, коли все сказано і зроблено, ViewModel запускає зворотний виклик для перегляду, щоб відобразити результати команди. Я все ще не можу залучити свою команду до роботи з використанням служб для обробки діалогових взаємодій у ViewModel.
Matthew S

15

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

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


THX, заголовок дійсно повинен міститись у моїй IDialogViewModel. інші властивості, такі як розмір, стандартна кнопка, я залишу, тому що все це виходить принаймні з шаблону даних.
blindmeis

1
Це те, що я робив і спочатку, просто використовую SizeToContent для контролю розміру вікна. Але в одному випадку мені потрібно було змінити розмір вікна, тому мені довелося трохи його підправити ...
Томас Левеск

@ThomasLevesque кнопки, що містяться у вашому ViewModel, це фактично об’єкти кнопки інтерфейсу або об’єкти, що представляють кнопки?
Томас

3
@Thomas, об'єкти, що представляють кнопки. Ніколи не слід посилатися на об’єкти інтерфейсу користувача у ViewModel.
Томас Левеск

2

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

  1. Я передаю посилання Module Controllerна конструктор кожного ViewModel(ви можете використовувати ін'єкцію).
  2. У ньому Module Controllerє публічні / внутрішні методи створення вікон діалогу (просто створення, не повертаючи результат). Отже, щоб відкрити вікно діалогу, ViewModelя пишу:controller.OpenDialogEntity(bla, bla...)
  3. Кожне вікно діалогу повідомляє про його результат (наприклад, Гаразд , Зберегти , Скасувати тощо) за допомогою слабких подій . Якщо ви використовуєте PRISM, то простіше публікувати сповіщення за допомогою цього EventAggregator .
  4. Для обробки результатів діалогу я використовую підписку на сповіщення (знову ж таки Слабкі події та EventAggregator у випадку ПРИЗМУ). Щоб зменшити залежність від таких сповіщень, використовуйте незалежні класи зі стандартними сповіщеннями.

Плюси:

  • Менший код. Я не проти використовувати інтерфейси, але я бачив занадто багато проектів, де надмірність використання інтерфейсів та шарів абстракції викликає більше проблем, ніж допомоги.
  • Відкриті вікна діалогу Module Controller- це простий спосіб уникнути чітких посилань і все ще дозволяє використовувати макети для тестування.
  • Повідомлення через слабкі події зменшують кількість потенційних витоків пам'яті.

Мінуси:

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

PS Я використовую цей підхід вже досить давно і готовий відстоювати свою придатність у коментарях та надавати приклади, якщо потрібно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.