On MVVM applications, one of the common problems is the fact that some of the commands that you want executed on the collection items reside on the main view model rather than the item itself. In Windows Runtime, this can be resolved with relative or absolute binding source with either attached properties or behavior SDK, however, on android with mvvm cross, it is a little harder to handle. In this post we will try to define a solution for exposing commands from the view model through the collection items themselves.
NOTE: For people who doesn’t have the time to go through the whole history lesson :), here is the link for the final code. And the other items in the series can be found here. |
Imagine we are working on a cross platform mobile application. The application is made up of a universal app that will be run on Windows 8 and Windows Phone 8.1. We also have a xamarin application as a counter part for the universal app.
When finally your colleague finishes up with the initial mockups, you start implementing the main page for the windows phone and windows store application.
In the third section of the application, we are supposed to load the data for the items from an external source. So we are going to make use of a view model for this section and re-use the view model for the windows phone application.
When the user clicks on one of the items we are supposed to navigate to the item detail page. And when the user selects an item, and clicks on the delete app bar button, the selected item should be removed from the source.
So we implement the view model as follows:
We have Items property to load in the source for the listview, we have the NavigateToItemCommand and DeleteItemCommand to deal with the respective actions. Finally, we have the SelectedItem property to bind to the selected item property.
So far everything works for the android phone, we use an MvxListView control to create a templated listview and bind the items as the source and ItemClick event bound to the NavigateToItemCommand. This works perfectly since the ItemClick command binding on MvxListView control brings the item itself.
However, in order to add the delete functionality, the design team decides to add a delete button on each item on the list view. Now, instead of binding the delete command on the main view model, we have to have a delete command on each item.
CommandableItem<T>
In this scenario, we can derive from the SampleDataItem and create a new class that will expose the commands on the item level. However, in order to create a generic solution for the problem, we can create a generic class:
public class CommandableItem<T> { private T m_Item; /// <summary> /// Used to expose parent commands on the item itself /// </summary> /// <param name="item">Item of type <typeparamref name="T"/></param> public CommandableItem(T item) { m_Item = item; } public T Item { get { return m_Item; } } }
So now in the ViewModel we have to use ObservableCollection<CommandableItem<SampleDataItem>> instead of the simple data collection. We also have to adjust the data load function of the ViewModel.
Now the bindings on the views should go through the Item property which exposes the SampleDataItem in this case.
In order to expose the commands, we can now extend the constructor. In order to support multiple commands we can have a dictionary of actions that has the Item itself as a parameter.
/// <summary> /// Used to expose parent commands on the item itself /// </summary> /// <param name="item">Item of type <typeparamref name="T"/></param> /// <param name="actions">Dictionary of actions to be exposed as Commands</param> public CommandableItem(T item, Dictionary<string,Action<T>> actions) { m_Item = item; if (actions != null && actions.Count > 0) { m_Actions = actions; m_Commands = m_Actions.ToDictionary( (pair) => pair.Key, (pair) => new MvxCommand(() => pair.Value(item))); } }
Notice that we are creating a second delegate for each action with the current item to be executed, and initializing an MvxCommand with this new delegate.
Now the binding for each command would follow the convention: {Binding Commands[CommandName]} (e.g. {Binding Commands[DeleteCommand]}. Same goes for the android bindings on item template:
<Button android:layout_width="@dimen/small_button" android:layout_height="@dimen/small_button" android:id="@+id/removeImage" android:layout_marginLeft="@dimen/small_margin" android:background="@drawable/icon_remove" local:MvxBind="Click Commands[DeleteCommand]" android:layout_centerVertical="true" android:layout_alignParentStart="true" />
Proving that this approach is working, we can clean-up the code a little bit.
First we can write an extension method for IEnumerable<T> to easily convert the items collection once we load them from the source into an observable collection of commandable items:
public static class CommandableExtensions { public static ObservableCollection<CommandableItem<T>> ToCommandableList<T>(this IEnumerable<T> list, Dictionary<string, Action<T>> commands) { // TODO: check for nulls; var newCollection = new ObservableCollection<CommandableItem<T>>(); foreach (var item in list) { newCollection.Add(new CommandableItem<T>(item, commands)); } return newCollection; } }
Another change we can make is to get rid of the complicated declarations of ObservableCollection<CommandableItem<T>>. Since the commandable items are generally going to be used in an observable collection we can create a new collection class:
public class CommandableList<T> : ObservableCollection<CommandableItem<T>> { }
And finally, when we want to remove an item from the collection (notice, the action will be accepting SampleDataItem, while the collection will be of type CommandableItem<SampleDataItem>), we need to implement the IEquatable interface to compare the actual item and an implicit cast operator so we can do the conversion from SampleDataItem to CommandableItem seamlessly.
#region IEquatable<T> Implementation /// <summary> /// Superficial comparision, implemented to compare items of type <typeparamref name="T"/> and Commandable<T> /// </summary> /// <param name="other"></param> /// <returns></returns> public bool Equals(CommandableItem<T> other) { return Item.Equals(other.Item); } #endregion /// <summary> /// Implicit Cast between Commandable<T> /// </summary> /// <remarks>This cast is very superficial, only used for comparison</remarks> /// <param name="item">Item to cast as CommandableItem</param> /// <returns></returns> public static implicit operator CommandableItem<T>(T item) { return new CommandableItem<T>(item); }
After these two methods are implemented, we can implement the ExecDeleteItem function as follows:
public void ExecDeleteItem(SampleDataItem sampleDataItem) { // TODO: Implement the service call to remove the item // For now using Task.Delay for service call Task.Delay(200).ContinueWith((task) => { m_Items.Remove(sampleDataItem); }); }
Notice type that the function is accepting and keep in mind that the m_Items is of type CommandableList<SampleDataItem> which derives from ObservableCollection<CommandableItem<T>>.
So final implementation for this commandable endeavor would look like:
public class CommandableList<T> : ObservableCollection<CommandableItem<T>> { } public class CommandableItem<T> : IEquatable<CommandableItem<T>> { private T m_Item; private readonly Dictionary<string, Action<T>> m_Actions; private Dictionary<string, MvxCommand> m_Commands; /// <summary> /// Used to expose parent commands on the item itself /// </summary> /// <param name="item">Item of type <typeparamref name="T"/></param> /// <param name="actions">Dictionary of actions to be exposed as Commadns</param> public CommandableItem(T item, Dictionary<string,Action<T>> actions) { m_Item = item; if (actions != null && actions.Count > 0) { m_Actions = actions; m_Commands = m_Actions.ToDictionary( (pair) => pair.Key, (pair) => new MvxCommand(() => pair.Value(item))); } } private CommandableItem(T item) { m_Item = item; } public Dictionary<string, MvxCommand> Commands { get { return m_Commands; } } public T Item { get { return m_Item; } } /// <summary> /// Superficial comparison, implemented to compare items of type <typeparamref name="T"/> and Commandable<T> /// </summary> /// <param name="other"></param> /// <returns></returns> public bool Equals(CommandableItem<T> other) { return Item.Equals(other.Item); } /// <summary> /// Implicit Cast between Commandable<T> /// </summary> /// <remarks>This cast is very superficial, only used for comparison</remarks> /// <param name="item">Item to cast as CommandableItem</param> /// <returns></returns> public static implicit operator CommandableItem<T>(T item) { return new CommandableItem<T>(item); } } public static class CommandableExtensions { public static CommandableList<T> ToCommandableList<T>(this IEnumerable<T> list, Dictionary<string, Action<T>> commands) { // TODO: check for nulls; var newCollection = new CommandableList<T>(); foreach (var item in list) { newCollection.Add(new CommandableItem<T>(item, commands)); } return newCollection; } }
Pingback: Developing Universal/Cross-Platform Apps with MVVM – IV | Can Bilgin
Pingback: Developing Universal/Cross-Platform Apps with MVVM – III | Can Bilgin
Pingback: Developing Universal/Cross-Platform Apps with MVVM – II | Can Bilgin
Pingback: Developing Universal/Cross-Platform Apps with MVVM – I | Can Bilgin
Pingback: Item Level Command Binding (MVVM)
Pingback: Developing Universal/Cross-Platform Apps with MVVM – VI | Can Bilgin
Pingback: Developing Universal/Cross-Platform Apps with MVVM – VII | Can Bilgin
Pingback: Xamarin.Forms application with MvvmCross – VIII | Can Bilgin