Binding iOS Views with MVVMCross

When using MVVMCross with Xamarin.iOS one occasionally runs into a case where we want our bindings to update controls that are native to the system but are not readily configured to support such a thing.  In these cases I often see people do a variety of things to handle this including listening for the PropertyChanged event on a view model.  I would like to share a better way that I found.

Let’s take an ActionSheet in iOS that we want to communicate with our ViewModel.  We could certainly write this to use events to communicate back to our view model, but doing so breaks the MVVM concept.  There is a better way.  First thing I do is define a custom class that takes a title string and the view the action sheet will be associated with.

        public BindableActionSheet(string title, UIView parentView)
        {
            _parentView = parentView;
            _actionSheet = new UIActionSheet(title);
            _actionSheet.Opaque = true;
            _actionSheet.Clicked += ActionSheetClicked;
        }

In this example, we are wiring up the Clicked event so we know when the user has clicked an item.  Next we need a way to show our options:

        private IList<string> _buttons;
        public IList<string> Buttons
        {
            get { return _buttons; }
            set
            {
                if (value != null)
                {
                    _buttons = value;
                    foreach (var day in value)
                        _actionSheet.AddButton(day);
                }
            }
        }

As you can see, when the property Buttons is set, we all a button for each string option.  Obviously, you can pass whatever values you want in through the setter.  However, you MUST have a getter, despite the fact that you will never use it, the binding doesn’t seem to work if its not there.

As for how you set it.  Well you would need to create your iOS binding set and ensure that you bind to a property on your view model:

      public override void ViewDidLoad ()
      {
         base.ViewDidLoad ();
         _bindableActionSheet = new BindableActionSheet("Select Conference Day", View);
         
         var set = this.CreateBindingSet<AgendaView, AgendaViewModel>();
         set.Bind(_bindableActionSheet).For(b => b.Buttons).To(x => x.ConferenceDates).OneWay();
         set.Bind(_bindableActionSheet).For(b => b.Show).To(x => x.DatesPopupVisible).OneWay();
         set.Bind(_bindableActionSheet).For(b => b.ItemSelectedCommand).To(x => x.DateSelectedCommand).OneWay();
         
         set.Apply();
      }

In this case, we are binding the Buttons property to the property ConferenceDates on the viewmodel.  Only communication from the ViewModel to the View will be permitted.  Within the AgendaViewModel the ConferenceDates property looks like this:

        public IList<string> ConferenceDates
        {
            get
            {
                return Presentations
                    .Select(x => x.StartTime.AsLongDisplayFormat())
                    .Distinct()
                    .ToList();
            }
        }

Immediately, you will notice this property does not have a setter.  This is intentional.  One of the nice things about MVVM is the ability to indicate when a property changes, even if you never set that property.  MVVMCross provides view models with a RaisePropertyChanged method which raises the PropertyChanged event the view is watching for.  When we raise this for a particular property, its getter is called.  In this case, the getter works with a source variable (Presentations) to provide a specific projection of this data.

Looking back at the binding set you can also see that I specified a binding for a command as well, ItemSelectedCommand.  This is nothing more than a simple property within the wrapper class.

        public ICommand ItemSelectedCommand { get; set; }
        
        void ActionSheetClicked (object sender, UIButtonEventArgs e)
        {
            DateTime theDate = Buttons[(int)e.ButtonIndex].AsDateTime();
            if (ItemSelectedCommand != null && ItemSelectedCommand.CanExecute(theDate))
            {
                ItemSelectedCommand.Execute(theDate);
            }
        }

By allowing the View Model to specify the command, I can have the event itself Execute it.  Just make sure you also check CanExecute to support not firing the command in certain cases.

The basic principle here is that by using a wrapper class we can easily bind existing views into our view model, regardless of whether or not they have a predefined binding setup.  In fact, I do this with many of the control I use on pages, including Navigation Title, Progress Dialogs, WebViews, and more.  It really makes your code cleaner and enables that nice separation that MVVM is built on.

Here is an example of a wrapper that enables binding of your View Controller Page title

    public class BindablePageTitle
    {
        private readonly UIViewController _viewController;

        public BindablePageTitle(UIViewController viewController)
        {
            _viewController = viewController;
        }

        private string _title;
        public string Title
        {
            get { return _title; }
            set
            {
                _title = value;
                _viewController.Title = value;
            }
        }
    }

2 thoughts on “Binding iOS Views with MVVMCross

Leave a comment