How to binding list item to whether it is containe

2019-01-20 13:53发布

问题:

The scenario is that we have a list of items displayed in a list (ListView on Android and UITableView on iOS). The ItemSource comes from a web service. In addition to the data properties of the item, each of the items has the additional state that it can be added to a collection of currently selected items which is reflected in another collection on the view model.

I need to be able to alter the display of the item in the list based on whether it is in the currently selected list.

The datacontext given for binding is the data that comes from the ItemsSource, which is jsut the raw data for an item and has no way of knowing if that data is one of the currently selected items. That has to be done by asking the ViewModel.

I'm struggling to find the best solution to this problem. I certainly do not want to modify the ItemsSource to add a bogus property to each item in the source.

I can hack something around it by overriding the Adapter/TableViewSource to hardcode this without actually using binding for this, but that seems wrong.

Update

That was not explained very well, so let me try to explain using an existing MvvmCross example as the basis.

Consider the books sample from N=6 and N=7. We want to add a new feature where we can mark the books that we already own and for those that we have marked it shows an indicator to tell you that you own the book.

Imagine that the view model has methods like:

public bool IsOwned(string id)
public void SetOwned(string id, bool owned)

The implementation of those is not important for this discussion, but I would probably also need some form of Event Handler to notify when ownership has changed for a book.

I am trying to figure out how I can set up a binding on the ListViewItem/TableViewCell so that it can query the IsOwned method on the ViewModel using the ID from the DataContext for the item to control visibility of a UI element in that record.

I'm guessing that I will have to create a custom source binding, but that binding has to reference the ViewModel and the DataContext so I am a little lost here.

Note that in my real problem I am trying to solve, the status of whether something is marked is temporary and local only to the ViewModel (if I exit the screen it is not remembered). This precludes the idea of having a Service for getting the information.

回答1:

Your UI list item cell has a DataContext that its controls bind to - that DataContext is the cell's own individual ViewModel.

MvvmCross doesn't (as of v3.1 today) let you look outside that DataContext - it doesn't let you (for example) inspect another UIElement's DataContext or doesn't let you ask for a Parent.

To work within this, I generally create my DataContext for my list item cell's so that they contain all the data and actions that those cells need - so that they are genuinely the ViewModel for the list item view. Where there is some convenient Model class to reuse, then this will often mean that I create a wrapper class to assist with the DataContext. Occasionally (rarely) I will do this in a ValueConverter (see below)*, but in general I will do this using wrapper classes exposed by the ViewModel instead.

For example, for something like your case, if I had a List<Foo> as my core model, then in the page level ViewModel I would create a FooWithSelectionState class:

public class FooWithSelectionState : MvxNotifyPropertyChanged
{
    public Foo Foo { get; set; /* normal INPC in here */ }
    public bool IsSelected { get; set; /* normal INPC in here */ }
}

This would then allow my ViewModel to expose a List<FooWithSelectedState> as a Property - and individual list item cells can then bind to IsSelected as well as to chained properties of the underlying object like Foo.Name.

Architecturally, this pattern is robust, it's testable, it's clean and it's "correct" in that each list item cell has its own well-defined ViewModel. However, I do understand it can feel a bit code-heavy - it can feel a bit overkill. In some cases, dev's just want to use the Model classes directly without a ViewModel step. Maybe in some future version of Mvx, then MvvmCross might add some alternative mechanism to allow DataBound UI elements to look outside of their own DataContext - this has been suggested in e.g. https://github.com/MvvmCross/MvvmCross/issues/35 - but today this doesn't exist (that I know of)


If you did want to use the ValueConverter route, then one way of doing this is to use a ValueConverter on the ItemsSource - for example, something like:

public class FooWrappingValueConverter : MvxValueConverter<IList<Foo>, IList<FooWithSelectionState>>
{
     protected override IList<FooWithSelectionState> Convert(IList<Foo> value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
     {
         var viewModel = parameter as MyViewModel;

         return value.Select(f => new FooWithSelectionState()
                                       {
                                          Foo = f,
                                          IsSelected = viewModel.IsSelected(f)
                                       })
                     .ToList();
     }
}

This could be used in binding with an expression like ItemsSource FooWrapping(SourceList, .) - but be careful using this in dynamic situations (where the list changes frequently) as this could be slow.