MvvmCross Dialog

2019-02-02 16:51发布

问题:

I am currently investigating all possible solutions to be able to inform the user, ie pop a dialog, when there is a decision he needs to make. This is a common problem with MVVM pattern and I am trying to solve it for MvvmCross framework.

Possible solutions could be:

  • Customize the MvxPresenter to be able to show dialogs, but that looks a bit ugly to me
  • Put a Dialog interface in the Core project and use Inversion of Control to inject the implementation from the UI project to the Core project
  • Use the MvxMessenger plugin and share messages between the Core and UI project. Sounds like a good idea but maybe more complicated to develop...

What would you suggest?

回答1:

Dialog input is an interesting topic that doesn't always fit well with the flow of Mvvm Data-Binding.

Generally, some use cases of Dialogs are for things like:

  1. adding a yes/no confirm option to a submit button
  2. requesting additional single input - e.g. a selection from a list
  3. offering a choice of actions (e.g. delete, edit or duplicate?)
  4. offering a confirmation message
  5. requesting additional complex input - e.g. collecting a set of firstname/lastname/age/accept_terms field

For some of these items, I'd suggest that mainly these could be modelled as purely View concerns. For example, requesting single item selection is commonly done from compound controls labels which display 'pickers' when tapped - e.g. like an MvxSpinner in https://github.com/slodge/MvvmCross-Tutorials/blob/master/ApiExamples/ApiExamples.Droid/Resources/Layout/Test_Spinner.axml#L16

For general cases, where you want the shared ViewModels to drive the user flow, then options which are available within MvvmCross include the 3 you list, all of which seem viable to me, but I agree that none of them is perfect.

As an additional suggestion, one nice architectural suggestion is from Microsoft's Pattern and Practices team. In http://msdn.microsoft.com/en-us/library/gg405494(v=pandp.40).aspx, they suggest a IInteractionRequest interface which can be used within data-binding especially for this type of situation.

Their reference implementation of this is:

public interface IInteractionRequest
{
    event EventHandler<InteractionRequestedEventArgs> Raised;
}

    public class InteractionRequestedEventArgs : EventArgs
    {
       public Action Callback { get; private set; }
       public object Context { get; private set; }
       public InteractionRequestedEventArgs(object context, Action callback)
       {
           Context = context;
           Callback = callback;
       }
    }

public class InteractionRequest<T> : IInteractionRequest
{
    public event EventHandler<InteractionRequestedEventArgs> Raised;

    public void Raise(T context, Action<T> callback)
    {
        var handler = this.Raised;
        if (handler != null)
        {
            handler(
                this, 
                new InteractionRequestedEventArgs(
                    context, 
                    () => callback(context)));
        }
    }
}

An example ViewModel use of this is:

private InteractionRequest<Confirmation> _confirmCancelInteractionRequest = new InteractionRequest<Confirmation>();
public IInteractionRequest ConfirmCancelInteractionRequest
{
    get
    {
        return _confirmCancelInteractionRequest;
    }
}

and the ViewModel can raise this using:

_confirmCancelInteractionRequest.Raise(
    new Confirmation("Are you sure you wish to cancel?"),
    confirmation =>
    {
        if (confirmation.Confirmed)
        {
            this.NavigateToQuestionnaireList();
        }
    });
}

where Confirmation is a simple class like:

    public class Confirmation
    {
        public string Message { get; private set; }
        public bool Confirmed { get; set; }
        public Confirmation(string message)
        {
           Message = message;
        }
    }

For using this in the Views:

The MSDN link shows how a Xaml client might bind to this using behaviours - so I won't cover this further here.

In iOS for MvvmCross, a View object might implement a property like:

private MvxGeneralEventSubscription _confirmationSubscription;
private IInteractionRequest _confirmationInteraction;
public IInteractionRequest ConfirmationInteraction
{
    get { return _confirmationInteraction; }
    set
    {
        if (_confirmationInteraction == value)
            return;
        if (_confirmationSubscription != null)
            _confirmationSubscription.Dispose();
        _confirmationInteraction = value;
        if (_confirmationInteraction != null)
            _confirmationSubscription = _confirmationInteraction
                .GetType()
                .GetEvent("Raised")
                .WeakSubscribe(_confirmationInteraction, 
                   DoConfirmation);
    }
}

This View property uses a WeakReference-based event subscription in order to channel ViewModel Raise events through to a View MessageBox-type method. It's important to use a WeakReference so that the ViewModel never has a reference to the View - these can cause memory leak issues in Xamarin.iOS. The actual MessageBox-type method itself would be fairly simple - something like:

private void DoConfirmation(InteractionRequestedEventArgs args)
{
    var confirmation = (Confirmation)args.Context;

    var alert = new UIAlertView(); 
    alert.Title = "Bazinga"; 
    alert.Message = confirmation.Message; 

    alert.AddButton("Yes"); 
    alert.AddButton("No"); 

    alert.Clicked += (sender, e) => { 
       var alertView = sender as UIAlertView; 

       if (e.ButtonIndex == 0) 
       { 
          // YES button
          confirmation.Confirmed = true;
       } 
       else if (e.ButtonIndex == 1) 
       { 
          // NO button
          confirmation.Confirmed = false; 
       } 

       args.Callback();
    }; 
}

And the property could be bound in a Fluent Binding set like:

set.Bind(this)
   .For(v => v.ConfirmationInteraction)
   .To(vm => vm.ConfirmCancelInteractionRequest);

For Android, a similar implementation could be used - this could perhaps use a DialogFragment and could perhaps also be bound using a View within XML.

Note:

  • I believe that the basic interaction could be improved (in my opinion) if we added further IInteractionRequest<T> and InteractionRequestedEventArgs<T> definitions - but, for the scope of this answer, I kept to the 'basic' implementation keeping as close as I could to the one presented in http://msdn.microsoft.com/en-us/library/gg405494(v=pandp.40).aspx
  • some additional helper classes could also help significantly simplify the view subscription code too


回答2:

You just can use MvvmCross UserInteraction plugin from Brian Chance



回答3:

As Eugene says, use the UserInteraction plugin. Unfortunately, there's not currently a Windows Phone implementation, so here's the code I've used in the interim:

public class WindowsPhoneUserInteraction : IUserInteraction
{
    public void Confirm(string message, Action okClicked, string title = null, string okButton = "OK", string cancelButton = "Cancel")
    {
        Confirm(message, confirmed =>
        {
            if (confirmed)
                okClicked();
        },
        title, okButton, cancelButton);
    }

    public void Confirm(string message, Action<bool> answer, string title = null, string okButton = "OK", string cancelButton = "Cancel")
    {
        var mbResult = MessageBox.Show(message, title, MessageBoxButton.OKCancel);
        if (answer != null)
            answer(mbResult == MessageBoxResult.OK);
    }

    public Task<bool> ConfirmAsync(string message, string title = "", string okButton = "OK", string cancelButton = "Cancel")
    {
        var tcs = new TaskCompletionSource<bool>();
        Confirm(message, tcs.SetResult, title, okButton, cancelButton);
        return tcs.Task;
    }

    public void Alert(string message, Action done = null, string title = "", string okButton = "OK")
    {
        MessageBox.Show(message, title, MessageBoxButton.OK);
        if (done != null)
            done();
    }

    public Task AlertAsync(string message, string title = "", string okButton = "OK")
    {
        var tcs = new TaskCompletionSource<object>();
        Alert(message, () => tcs.SetResult(null), title, okButton);
        return tcs.Task;
    }

    public void Input(string message, Action<string> okClicked, string placeholder = null, string title = null, string okButton = "OK", string cancelButton = "Cancel", string initialText = null)
    {
        throw new NotImplementedException();
    }

    public void Input(string message, Action<bool, string> answer, string placeholder = null, string title = null, string okButton = "OK", string cancelButton = "Cancel", string initialText = null)
    {
        throw new NotImplementedException();
    }

    public Task<InputResponse> InputAsync(string message, string placeholder = null, string title = null, string okButton = "OK", string cancelButton = "Cancel", string initialText = null)
    {
        throw new NotImplementedException();
    }

    public void ConfirmThreeButtons(string message, Action<ConfirmThreeButtonsResponse> answer, string title = null, string positive = "Yes", string negative = "No", string neutral = "Maybe")
    {
        throw new NotImplementedException();
    }

    public Task<ConfirmThreeButtonsResponse> ConfirmThreeButtonsAsync(string message, string title = null, string positive = "Yes", string negative = "No", string neutral = "Maybe")
    {
        throw new NotImplementedException();
    }
}

You'll notice that not everything's implemented, and even those bits that are are limited (you can't set the OK ad Cancel button text, for example)

Of course, I needed to register this in setup.cs as well:

Mvx.RegisterSingleton<IUserInteraction>(new WindowsPhoneUserInteraction());