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:
What would you suggest?
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:
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:
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