Boas ou más práticas para caixas de diálogo no wpf com MVVM?

148

Ultimamente, tive o problema de criar caixas de diálogo de adição e edição para o meu aplicativo wpf.

Tudo o que eu quero fazer no meu código é algo como isto. (Eu uso principalmente a primeira abordagem do viewmodel com o mvvm)

ViewModel que chama uma janela de diálogo:

var result = this.uiDialogService.ShowDialog("Dialogwindow Title", dialogwindowVM);
// Do anything with the dialog result

Como funciona?

Primeiro, criei um serviço de diálogo:

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é uma janela especial, mas simples. Preciso manter meu conteúdo:

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

Um problema com as caixas de diálogo no wpf é dialogresult = trueque só pode ser alcançado no código. Por isso, criei uma interface para dialogviewmodelimplementá-la.

public class RequestCloseDialogEventArgs : EventArgs
{
    public bool DialogResult { get; set; }
    public RequestCloseDialogEventArgs(bool dialogresult)
    {
        this.DialogResult = dialogresult;
    }
}

public interface IDialogResultVMHelper
{
    event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
}

Sempre que meu ViewModel achar que está na hora dialogresult = true, aumente esse evento.

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

Agora, pelo menos, tenho que criar um DataTemplateno meu arquivo de recursos ( app.xamlou algo assim):

<DataTemplate DataType="{x:Type DialogViewModel:EditOrNewAuswahlItemVM}" >
        <DialogView:EditOrNewAuswahlItem/>
</DataTemplate>

Bem, isso é tudo, agora posso chamar diálogos dos meus modelos de exibição:

 var result = this.uiDialogService.ShowDialog("Dialogwindow Title", dialogwindowVM);

Agora minha pergunta, você vê algum problema com esta solução?

Edit: para completar. O ViewModel deve implementar IDialogResultVMHelpere, em seguida, pode aumentá-lo dentro de OkCommandalgo ou algo como isto:

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

EDIÇÃO 2: Usei o código aqui para tornar meu EventHandler fraco:
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(o site não existe mais, o 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
fonte
1
você provavelmente está perdendo a referência xmlns: x = " schemas.microsoft.com/winfx/2006/xaml " no seu WindowDialog XAML.
Adiel Yaacov
Na verdade, o namespace é xmlns: x = "[http: //] schemas.microsoft.com/winfx/2006/xaml" sem os colchetes
reggaeguitar
1
Oi! Atraso aqui. Não estou entendendo como seu Serviço tem uma referência ao WindowDialog. Qual é a hierarquia dos seus modelos? Na minha opinião, o View contém uma referência ao assembly Viewmodel e o Viewmodel aos assemblies Service e Model. Dessa forma, a camada Serviço não teria conhecimento da exibição WindowDialog. o que estou perdendo?
Moe45673
2
Oi @blindmeis, apenas tentando entender esse conceito, não acho que exista algum exemplo de projeto on-line que eu possa escolher? Estou confuso sobre várias coisas.
Hank

Respostas:

48

Essa é uma boa abordagem e eu usei similares no passado. Vá em frente!

Uma coisa menor que eu definitivamente faria é fazer com que o evento receba um valor booleano para quando você precisar definir "false" no DialogResult.

event EventHandler<RequestCloseEventArgs> RequestCloseDialog;

e a classe EventArgs:

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

    public bool DialogResult { get; private set; }
}
Julian Dominguez
fonte
E se, em vez de usar serviços, alguém usar um tipo de retorno de chamada para facilitar a interação com o ViewModel e o View? Por exemplo, o View executa um comando no ViewModel; quando tudo estiver pronto, o ViewModel acionará um retorno de chamada para o View para exibir os resultados do comando. Ainda não consigo integrar minha equipe ao usar os Serviços para lidar com interações de Diálogo no ViewModel.
Matthew S
15

Estou usando uma abordagem quase idêntica há vários meses e estou muito feliz com ela (ou seja, ainda não senti vontade de reescrevê-la completamente ...)

Na minha implementação, eu uso um IDialogViewModelque expõe coisas como o título, os botões do standad para mostrar (para ter uma aparência consistente em todas as caixas de diálogo), um RequestCloseevento e algumas outras coisas para controlar o tamanho da janela e comportamento

Thomas Levesque
fonte
thx, o título deve realmente ir no meu IDialogViewModel. as outras propriedades como tamanho, botão padrão deixarei, porque tudo isso vem pelo menos do datatemplate.
Blindmeis 28/09/10
1
Também foi o que fiz no início, basta usar SizeToContent para controlar o tamanho da janela. Mas em um caso que eu precisava para fazer o redimensionável janela, então eu tive que ajustá-lo um pouco ...
Thomas Levesque
@ThomasLevesque os botões contidos no seu ViewModel, eles são realmente objetos de botão da interface do usuário ou objetos que representam botões?
Thomas
3
@ Thomas, objetos representando botões. Você nunca deve fazer referência a objetos de interface do usuário no ViewModel.
Thomas Levesque
2

Se você está falando sobre janelas de diálogo e não apenas sobre as caixas de mensagem pop-up, considere minha abordagem abaixo. Os pontos principais são:

  1. Eu passo uma referência para Module Controllero construtor de cada um ViewModel(você pode usar a injeção).
  2. Que Module Controllerpossui métodos públicos / internos para criar janelas de diálogo (apenas criando, sem retornar um resultado). Portanto, para abrir uma janela de diálogo, ViewModelescrevo:controller.OpenDialogEntity(bla, bla...)
  3. Cada janela de diálogo notifica seu resultado (como OK , Salvar , Cancelar etc.) por meio de Eventos Fracos . Se você usa o PRISM, é mais fácil publicar notificações usando este EventAggregator .
  4. Para lidar com os resultados do diálogo, estou usando a assinatura de notificações (novamente Weak Events e EventAggregator no caso de PRISM). Para reduzir a dependência de tais notificações, use classes independentes com notificações padrão.

Prós:

  • Menos código. Não me importo em usar interfaces, mas já vi muitos projetos em que o excesso de uso de interfaces e camadas de abstração causa mais problemas do que ajuda.
  • As janelas de diálogo abertas Module Controllersão uma maneira simples de evitar referências fortes e ainda permitem usar modelos para testes.
  • A notificação através de eventos fracos reduz o número de possíveis vazamentos de memória.

Contras:

  • Não é fácil distinguir a notificação necessária das outras pessoas no manipulador. Duas soluções:
    • envie um token exclusivo ao abrir uma janela de diálogo e verifique esse token na assinatura
    • use classes de notificação genéricas em <T>que Thá uma enumeração de entidades (ou, por simplicidade, pode ser o tipo de ViewModel).
  • Para um projeto, deve haver um acordo sobre o uso de classes de notificação para evitar duplicá-las.
  • Para projetos enormemente grandes, os Module Controllermétodos de criação de janelas podem ser sobrecarregados. Nesse caso, é melhor dividi-lo em vários módulos.

PS: Eu tenho usado essa abordagem há muito tempo e estou pronto para defender sua elegibilidade nos comentários e fornecer alguns exemplos, se necessário.

Alex Klaus
fonte