Mark Erickson
Mark Erickson

Reputation: 797

Using MvvmCross how can I bind a list of annotations to a MapView?

Lets say I have a MapView which contains a property "Annotations". To get annotations on the MapView you have to use either AddAnotation or AddAnotations.

public class SiteItems
{
    public string Title { get; set; }
    public string SubTitle { get; set; }

    public string Phone { get; set; }
    public string Address { get; set; }
    public string Url { get; set; }

    public double Latitude { get; set; }
    public double Longitude { get; set; }
}

Then I have a ViewModel of:

public class SiteViewModel : MvxViewModel
{

    private IObservableCollection<Models.SiteItems> _siteItems;
    public IObservableCollection<Models.SiteItems> SiteItems {
        get{ return _siteItems; }
        set{ _siteItems = value; 
            RaisePropertyChanged (() => SiteItems);
        }
    }
}

I also have a converter which will convert SiteItem to MKAnnotation

So I guess my question is how do I bind something like this since we can not bind directly to "Annotations" property? Do I bind to a command?

Thanks and any help is appreciated!

Upvotes: 4

Views: 2880

Answers (1)

Stuart
Stuart

Reputation: 66882

Subscribing to changing collections is one of the corner-stones of Data-Binding and relies on a little knowledge of the INotifyCollectionChanged interface.

Within the MvvmCross source, there are a few example classes which show how to subscribe to collections and their change notifications - e.g. MvxViewGroupExtensions.cs in Droid and MvxTableViewSource.cs in Touch

The core to the technique is to create an Adapter or Source object which listens for changes either in the whole list or to parts of the list and which takes action accordingly.

The same type of approach applies for maps with multiple - but markers - although we don't yet have any helper classes for this.


Without actually having a Mac or iOS device to hand, here are roughly the steps I'd take to create a wrapper...

Assuming I had a Model object like:

public class House
{
    public double Lat { get; set; }
    public double Lng { get; set; }
    public string Name { get; set; }
}

Inside a ViewModel like:

public class FirstViewModel : MvxViewModel
{
    public ObservableCollection<House> HouseList { get; set; }
}

With this done, then in the View we can create an annotation class for each House - e.g. something like:

public class HouseAnnotation : MKAnnotation
{
    public HouseAnnotation(House house)
    {
        // Todo - the details of actually using the house here.
        // in theory you could also data-bind to the house too (e.g. if it's location were to move...)
    }

    public override CLLocationCoordinate2D Coordinate { get; set; }
}

We could then create a HouseAnnotationManager who's responsibility would be to manage the translation of changes in the HouseList being mapped to changes in the annotations being displayed on the map.

To do this, we would give the manager methods to:

  1. Create a single annotation:

    private MKAnnotation CreateAnnotation(House house)
    {
        return new HouseAnnotation(house);
    }
    
  2. Add an annotation to the map (and to a local lookup table)

    private void AddAnnotationFor(House house)
    {
        var annotation = CreateAnnotation(house);
        _annotations[house] = annotation;
        _mapView.AddAnnotation(annotation);
    }
    
  3. Remove an annotation from the map (and from a local lookup table)

    private void RemoveAnnotationFor(House house)
    {
        var annotation = _annotations[house];
        _mapView.RemoveAnnotation(annotation);
        _annotations.Remove(house);
    }
    
  4. Do the same actions for lists:

    private void AddAnnotations(IList newItems)
    {
        foreach (House house in newItems)
        {
            AddAnnotationFor(house);
        }
    }
    
    private void RemoveAnnotations(IList oldItems)
    {
        foreach (House house in oldItems)
        {
            RemoveAnnotationFor(house);
        }
    }
    
  5. Respond to INotifyCollection changes:

    private void OnItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                AddAnnotations(e.NewItems);
                break;
            case NotifyCollectionChangedAction.Remove:
                RemoveAnnotations(e.OldItems);
                break;
            case NotifyCollectionChangedAction.Replace:
                RemoveAnnotations(e.OldItems);
                AddAnnotations(e.NewItems);
                break;
            case NotifyCollectionChangedAction.Move:
                // not interested in this
                break;
            case NotifyCollectionChangedAction.Reset:
                ReloadAllAnnotations();
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
    
  6. Respond to whole list changes:

    // MvxSetToNullAfterBinding isn't strictly needed any more 
    // - but it's nice to have for when binding is torn down
    [MvxSetToNullAfterBinding]
    public virtual IEnumerable<House> ItemsSource
    {
        get { return _itemsSource; }
        set { SetItemsSource(value); }
    }
    
    protected virtual void SetItemsSource(IEnumerable<House> value)
    {
        if (_itemsSource == value)
            return;
    
        if (_subscription != null)
        {
            _subscription.Dispose();
            _subscription = null;
        }
        _itemsSource = value;
        if (_itemsSource != null && !(_itemsSource is IList))
            MvxBindingTrace.Trace(MvxTraceLevel.Warning,
                                  "Binding to IEnumerable rather than IList - this can be inefficient, especially for large lists");
    
        ReloadAllAnnotations();
    
        var newObservable = _itemsSource as INotifyCollectionChanged;
        if (newObservable != null)
        {
            _subscription = newObservable.WeakSubscribe(OnItemsSourceCollectionChanged);
        }
    }
    

With this all written, then your ViewModel can have a private _manager field and can create and data-bind it as:

        _manager = new HouseAnnotationManager(myMapView);

        var set = this.CreateBindingSet<FirstView, FirstViewModel>();
        set.Bind(_manager).To(vm => vm.HouseList);
        set.Apply();

Overall, this might look something like: https://gist.github.com/slodge/6070386

Disclaimer: this code hasn't been compiled let alone run, but the approach is basically correct (I think)

Note: if this does/doesn't work with some fixing, I'd quite like it submitted back to the Mvx community as a sample ;)


The same basic approach should also work in Android - although in Android you'll also have to fight a battle against the setup - Ant, Google Play v2 and all that jazz.


If you wanted to do further map manipulations - e.g. changing the map center and zoom when a house is added, then this can obviously be done from within overrides of methods such as AddAnnotation within your manager.

Upvotes: 6

Related Questions