1
votes

I'm trying to bind a custom command to UIAlertController (ios 8). I have a button in the nav bar and an action sheet attached to it. When user clicks the nav bar button, the action sheet appears. When user clicks the action sheet button, he must be redirected to another view / view model.

My view model code:

public ICommand AddPhonecallCommand
{
    get
    {
        return new MvxCommand(() => ShowViewModel<AddPhonecallViewModel>();
    }
}

public ICommand AddMeetingCommand
{
    get
    {
        return new MvxCommand(() => ShowViewModel<AddMeetingViewModel>();
    }
}

My view code:

var actionSheet = UIAlertController.Create("Add a new...", null, UIAlertControllerStyle.ActionSheet);

actionSheet.AddAction(UIAlertAction.Create("Phone call", UIAlertActionStyle.Default, null));
actionSheet.AddAction(UIAlertAction.Create("Meeting", UIAlertActionStyle.Default, null));

var rightNavButton = new UIBarButtonItem("Add", UIBarButtonItemStyle.Plain, (s, e) =>
            {
                this.PresentViewController(actionSheet, true, null);
            });

So, every button in the action sheet should redirect to a specific view model. But it seems that buttons in the action sheet are not UIButtons. So I need somehow bind UIAlertAction to ICommand.

set.Bind(...).For(...).To(vm => vm.AddPhonecallCommand);

What should I put instead of the dots?


I don’t have an instance of the view model in the view. I’m using this syntax:

var set = this.CreateBindingSet<MainView, MainViewModel>();
set.Bind(source).To(vm => vm.Events); // binding IEnumerable
set.Apply();

There is no direct instantiation of the viewmodel. The framework does all dirty work for me. So, if I'm writing

set.Bind(rightNavButton).To(vm => vm.AddPhonecallCommand);  // binding the Clicked event

everything works just fine. But if I'm tryng to write something like this

var actionSheetButton = UIAlertAction.Create("Phone call", UIAlertActionStyle.Default, null));
...
set.Bind(actionSheetButton).To(vm => vm.AddPhonecallCommand);  // attempt to bind

nothing happens. Probably because we don’t have suitable events in UIAlertAction at all.

3
I'm not sure I understand what the "binding" is you are trying to achieve. Can you edit the question to explain what sort of ViewModel properties and/or commands you are trying to bind to. (I'm really not sure if you are trying to bind the nav bar button, the action sheet buttons or something else - sorry!) - Stuart
I've edited the question, hope it helps. - synoptic

3 Answers

1
votes

If all you want to do is to execute those commands, then an easy route is to use:

 actionSheet.AddAction(UIAlertAction.Create("Phone call", UIAlertActionStyle.Default, (s,e) => {
     MyViewModel.AddPhoneCallCommand.Execute(null);
 }));

If you want "full binding" - including observing viewmodel changes and observing CanExecute changes, then this can also be done - you'll need to persist the UIAlertAction instances and to bind properties on them to the ViewModel.

0
votes

I found a suitable solution. I created a proxy class for UIAlertAction:

public class UIAlertActionBindable : UIAlertAction
{
    public UIAlertAction AlertAction;

    public UIAlertActionBindable(string title, UIAlertActionStyle style)
    {
        AlertAction = UIAlertAction.Create(title, style, action =>
            {
                if(Clicked != null)
                {
                    Clicked(this, null);
                }
            });
    }

    public event EventHandler Clicked;
}

Then I created a custom binding class

public class UIAlertActionBinding : MvxTargetBinding
{
    private readonly UIAlertActionBindable _view;
    private IMvxCommand _command;

    public UIAlertActionBinding(UIAlertActionBindable view) 
        : base(view)
    {
        _view = view;
        _view.Clicked += OnClicked;
    }

    void OnClicked(object sender, EventArgs e)
    {
        if (_command != null)
        {
            _command.Execute();
        }
    }

    public override void SetValue(object value)
    {
        _command = (IMvxCommand)value;
    }

    protected override void Dispose(bool isDisposing)
    {
        if (isDisposing)
        {
            _view.Clicked -= OnClicked;
        }
        base.Dispose(isDisposing);   
    }

    public override Type TargetType
    {
        get
        {
            return typeof(IMvxCommand);
        }
    }

    public override Cirrious.MvvmCross.Binding.MvxBindingMode DefaultMode
    {
        get
        {
            return MvxBindingMode.OneWay;
        }
    }
}

and modified Setup.cs:

protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
    {
        base.FillTargetFactories(registry);

        registry.RegisterFactory(
            new MvxCustomBindingFactory<UIAlertActionBindable>("Click", aab => new UIAlertActionBinding(aab)));
    }

The code in the view looks like:

var actionSheet = UIAlertController.Create("Add a new...", null, UIAlertControllerStyle.ActionSheet);
var meetingActionButton = new UIAlertActionBindable("Meeting", UIAlertActionStyle.Default);
actionSheet.AddAction(meetingActionButton.AlertAction);
...
set.Bind(meetingActionButton).For("Click").To(vm => vm.AddMeetingCommand);

Everything works like a charm.

0
votes

Why not create a cross platform service to deal with this? This way you can call for the window a bit easier from your view model as well and you can use it in android, winphone, and iOS.

public class Option {
    public string Text { get; set; }
    public Action Action { get; set; }

    public Option(string text, Action action) {
        Text = text;
        Action = action;
    }
}

public interface IDialogService {
    void ActionSheet(string title, params Option[] option);
}

public class iOSDialogService : IDialogService {

    public void ActionSheet(string title, params Option[] options) {
        var sheet = UIAlertController.Create(title ?? String.Empty, String.Empty, UIAlertControllerStyle.ActionSheet);
        foreach (var opt in options) {
            sheet.AddAction(UIAlertAction.Create(opt.Text, UIAlertActionStyle.Default, x => {
                opt.Action();
            }))
        );

    }


    private void Present(UIAlertController controller) {
        this.Dispatch(() =>  {
            var top = GetTopViewController();
            var po = controller.PopoverPresentationController;
            if (po != null) {
                po.SourceView = top.View;
                var h = (top.View.Frame.Height / 2) - 400;
                var v = (top.View.Frame.Width / 2) - 300;
                po.SourceRect = new RectangleF(v, h, 0, 0);
                po.PermittedArrowDirections = UIPopoverArrowDirection.Any;
            }
            top.PresentViewController(controller, true, null);
        });
     }


     private static UIViewController GetTopViewController() {
        var root = UIApplication
            .SharedApplication
            .Windows
            .Reverse()
            .FirstOrDefault(x => 
                x.WindowLevel == UIWindowLevel.Normal && 
                !x.Hidden
            )
            .RootViewController;

        var tabs = root as UITabBarController;
        if (tabs != null)
            return tabs.SelectedViewController;

        var nav = root as UINavigationController;
        if (nav != null)
            return nav.VisibleViewController;

        if (root.PresentedViewController != null)
            return root.PresentedViewController;

        return root;
     }
}

Now to call it, simply inject this service into your view model:

public IMvxCommand Choice {
    get {
        return new MvxCommand(() => dialogService.ActionSheet(
            "Action Sheet",
            new Option("Button1", () => { .. do something here }),
            new Option("Button2", () => { .. do something else here })   
        ));
    }
}

For a more complete solution, search for "MvvmCross User Dialogs" on nuget or go take a look at it here