Added ObservableTableViewSource and ObservableCollectionViewSource with corresponding

extension methods and unit tests.
This commit is contained in:
Laurent Bugnion 2016-02-11 17:20:20 +01:00
Родитель 7a2877bf34
Коммит e1358e670f
6 изменённых файлов: 1394 добавлений и 18 удалений

Просмотреть файл

@ -68,6 +68,8 @@
<Compile Include="Helpers\BindingGenericApple.cs" />
<Compile Include="Helpers\ExtensionsApple.cs" />
<Compile Include="Helpers\ObservableTableViewController.cs" />
<Compile Include="Helpers\ObservableCollectionViewSource.cs" />
<Compile Include="Helpers\ObservableTableViewSource.cs" />
<Compile Include="Threading\DispatcherHelper.cs" />
<Compile Include="Views\ControllerBase.cs" />
<Compile Include="Views\DialogService.cs" />

Просмотреть файл

@ -28,25 +28,113 @@ namespace GalaSoft.MvvmLight.Helpers
public static class ExtensionsApple
{
/// <summary>
/// Creates a new <see cref="ObservableTableViewController{T}"/> for a given <see cref="ObservableCollection{T}"/>.
/// Creates a new <see cref="ObservableCollectionViewSource{TItem, TCell}"/> for a given <see cref="IList{TItem}"/>.
/// Note that if the IList doesn't implement INotifyCollectionChanged, the associated UICollectionView won't be
/// updated when the IList changes.
/// </summary>
/// <typeparam name="T">The type of the items contained in the collection.</typeparam>
/// <typeparam name="TItem">The type of the items in the IList.</typeparam>
/// <typeparam name="TCell">The type of cells in the CollectionView associated to this ObservableCollectionViewSource.</typeparam>
/// <param name="list">The IList that should be represented in the associated UICollectionView</param>
/// <param name="bindCellDelegate">A delegate to a method taking a <see cref="UICollectionViewCell"/>
/// and setting its elements' properties according to the item passed as second parameter.</param>
/// <param name="getSupplementaryViewDelegate">A delegate to a method returning a <see cref="UICollectionReusableView"/>
/// and used to set supplementary views on the UICollectionView.</param>
/// <param name="reuseId">An ID used for optimization and cell reuse.</param>
/// <param name="factory">An optional delegate returning an instance of a class deriving from
/// <see cref="ObservableCollectionViewSource{TItem, TCell}"/>. This can be used if you need to implement
/// specific features in addition to the built-in features of ObservableCollectionViewSource.</param>
/// <returns>The new instance of ObservableCollectionViewSource.</returns>
public static ObservableCollectionViewSource<TItem, TCell> GetCollectionViewSource<TItem, TCell>(
this IList<TItem> list,
Action<TCell, TItem, NSIndexPath> bindCellDelegate,
Func<NSString, NSIndexPath, UICollectionReusableView> getSupplementaryViewDelegate = null,
string reuseId = null,
Func<ObservableCollectionViewSource<TItem, TCell>> factory = null)
where TCell : UICollectionViewCell
{
if (factory != null)
{
var coll = factory();
coll.DataSource = list;
coll.BindCellDelegate = bindCellDelegate;
coll.GetSupplementaryViewDelegate = getSupplementaryViewDelegate;
coll.ReuseId = reuseId;
return coll;
}
return new ObservableCollectionViewSource<TItem, TCell>
{
DataSource = list,
BindCellDelegate = bindCellDelegate,
GetSupplementaryViewDelegate = getSupplementaryViewDelegate,
ReuseId = reuseId
};
}
/// <summary>
/// Creates a new <see cref="ObservableCollectionViewSource{TItem, TCell}"/> for a given <see cref="ObservableCollection{TItem}"/>.
/// The associated UICollectionView will be updated when the ObservableCollection changes.
/// </summary>
/// <typeparam name="TItem">The type of the items in the IList.</typeparam>
/// <typeparam name="TCell">The type of cells in the CollectionView associated to this ObservableCollectionViewSource.</typeparam>
/// <param name="list">The ObservableCollection that should be represented in the associated UICollectionView</param>
/// <param name="bindCellDelegate">A delegate to a method taking a <see cref="UICollectionViewCell"/>
/// and setting its elements' properties according to the item passed as second parameter.</param>
/// <param name="getSupplementaryViewDelegate">A delegate to a method returing a <see cref="UICollectionReusableView"/>
/// and used to set supplementary views on the UICollectionView.</param>
/// <param name="reuseId">An ID used for optimization and cell reuse.</param>
/// <param name="factory">An optional delegate returning an instance of a class deriving from
/// <see cref="ObservableCollectionViewSource{TItem, TCell}"/>. This can be used if you need to implement
/// specific features in addition to the built-in features of ObservableCollectionViewSource.</param>
/// <returns>The new instance of ObservableCollectionViewSource.</returns>
public static ObservableCollectionViewSource<TItem, TCell> GetCollectionViewSource<TItem, TCell>(
this ObservableCollection<TItem> list,
Action<TCell, TItem, NSIndexPath> bindCellDelegate,
Func<NSString, NSIndexPath, UICollectionReusableView> getSupplementaryViewDelegate = null,
string reuseId = null,
Func<ObservableCollectionViewSource<TItem, TCell>> factory = null)
where TCell : UICollectionViewCell
{
if (factory != null)
{
var coll = factory();
coll.DataSource = list;
coll.BindCellDelegate = bindCellDelegate;
coll.GetSupplementaryViewDelegate = getSupplementaryViewDelegate;
coll.ReuseId = reuseId;
return coll;
}
return new ObservableCollectionViewSource<TItem, TCell>
{
DataSource = list,
BindCellDelegate = bindCellDelegate,
GetSupplementaryViewDelegate = getSupplementaryViewDelegate,
ReuseId = reuseId
};
}
/// <summary>
/// Creates a new <see cref="ObservableTableViewController{TItem}"/> for a given <see cref="ObservableCollection{TItem}"/>.
/// </summary>
/// <typeparam name="TItem">The type of the items contained in the collection.</typeparam>
/// <param name="collection">The collection that the adapter will be created for.</param>
/// <param name="createCellDelegate">A delegate to a method creating or reusing a <see cref="UITableViewCell"/>.
/// The cell will then be passed to the bindCellDelegate
/// delegate to set the elements' properties.</param>
/// The cell will then be passed to the bindCellDelegate delegate to set the elements' properties.
/// If you use a reuseId, you can pass null for the createCellDelegate.</param>
/// <param name="bindCellDelegate">A delegate to a method taking a <see cref="UITableViewCell"/>
/// and setting its elements' properties according to the item passed as second parameter.
/// The cell must be created first in the createCellDelegate delegate, unless a <see cref="reuseId"/> is passed to the method.</param>
/// The cell must be created first in the createCellDelegate delegate, unless a
/// reuseId is passed to the method.</param>
/// <param name="reuseId">A reuse identifier for the TableView's cells.</param>
/// <returns>A controller adapted to the collection passed in parameter.</returns>
public static ObservableTableViewController<T> GetController<T>(
this ObservableCollection<T> collection,
public static ObservableTableViewController<TItem> GetController<TItem>(
this ObservableCollection<TItem> collection,
Func<NSString, UITableViewCell> createCellDelegate,
Action<UITableViewCell, T, NSIndexPath> bindCellDelegate,
Action<UITableViewCell, TItem, NSIndexPath> bindCellDelegate,
string reuseId = null)
{
return new ObservableTableViewController<T>
return new ObservableTableViewController<TItem>
{
DataSource = collection,
CreateCellDelegate = createCellDelegate,
@ -56,25 +144,26 @@ namespace GalaSoft.MvvmLight.Helpers
}
/// <summary>
/// Creates a new <see cref="ObservableTableViewController{T}"/> for a given <see cref="IList{T}"/>.
/// Creates a new <see cref="ObservableTableViewController{TItem}"/> for a given <see cref="IList{TItem}"/>.
/// </summary>
/// <typeparam name="T">The type of the items contained in the list.</typeparam>
/// <typeparam name="TItem">The type of the items contained in the list.</typeparam>
/// <param name="list">The list that the adapter will be created for.</param>
/// <param name="createCellDelegate">A delegate to a method creating or reusing a <see cref="UITableViewCell"/>.
/// The cell will then be passed to the bindCellDelegate
/// delegate to set the elements' properties.</param>
/// The cell will then be passed to the bindCellDelegate delegate to set the elements' properties.
/// If you use a reuseId, you can pass null for the createCellDelegate.</param>
/// <param name="bindCellDelegate">A delegate to a method taking a <see cref="UITableViewCell"/>
/// and setting its elements' properties according to the item passed as second parameter.
/// The cell must be created first in the createCellDelegate delegate, unless a <see cref="reuseId"/> is passed to the method.</param>
/// The cell must be created first in the createCellDelegate delegate, unless a reuseId is
/// passed to the method.</param>
/// <param name="reuseId">A reuse identifier for the TableView's cells.</param>
/// <returns>A controller adapted to the collection passed in parameter.</returns>
public static ObservableTableViewController<T> GetController<T>(
this IList<T> list,
public static ObservableTableViewController<TItem> GetController<TItem>(
this IList<TItem> list,
Func<NSString, UITableViewCell> createCellDelegate,
Action<UITableViewCell, T, NSIndexPath> bindCellDelegate,
Action<UITableViewCell, TItem, NSIndexPath> bindCellDelegate,
string reuseId = null)
{
return new ObservableTableViewController<T>
return new ObservableTableViewController<TItem>
{
DataSource = list,
CreateCellDelegate = createCellDelegate,
@ -83,6 +172,166 @@ namespace GalaSoft.MvvmLight.Helpers
};
}
/// <summary>
/// Creates a new <see cref="ObservableTableViewSource{TItem}"/> for a given <see cref="IList{TItem}"/>.
/// Note that if the IList doesn't implement INotifyCollectionChanged, the associated UITableView won't be
/// updated when the IList changes.
/// </summary>
/// <typeparam name="TItem">The type of the items in the IList.</typeparam>
/// <param name="list">The IList that should be represented in the associated UITableView</param>
/// <param name="bindCellDelegate">A delegate to a method taking a <see cref="UITableViewCell"/>
/// and setting its elements' properties according to the item passed as second parameter.</param>
/// <param name="reuseId">An ID used for optimization and cell reuse.</param>
/// <param name="factory">An optional delegate returning an instance of a class deriving from
/// <see cref="ObservableTableViewSource{TItem}"/>. This can be used if you need to implement
/// specific features in addition to the built-in features of ObservableTableViewSource.</param>
/// <returns>The new instance of ObservableTableViewSource.</returns>
public static ObservableTableViewSource<TItem> GetTableViewSource<TItem>(
this IList<TItem> list,
Action<UITableViewCell, TItem, NSIndexPath> bindCellDelegate,
string reuseId = null,
Func<ObservableTableViewSource<TItem>> factory = null)
{
if (factory != null)
{
var coll = factory();
coll.DataSource = list;
coll.BindCellDelegate = bindCellDelegate;
coll.ReuseId = reuseId;
return coll;
}
return new ObservableTableViewSource<TItem>
{
DataSource = list,
BindCellDelegate = bindCellDelegate,
ReuseId = reuseId
};
}
/// <summary>
/// Creates a new <see cref="ObservableTableViewSource{TItem}"/> for a given <see cref="ObservableCollection{TItem}"/>.
/// The associated UITableView will be updated when the ObservableCollection changes.
/// </summary>
/// <typeparam name="TItem">The type of the items in the IList.</typeparam>
/// <param name="list">The ObservableCollection that should be represented in the associated UITableView</param>
/// <param name="bindCellDelegate">A delegate to a method taking a <see cref="UITableViewCell"/>
/// and setting its elements' properties according to the item passed as second parameter.</param>
/// <param name="reuseId">An ID used for optimization and cell reuse.</param>
/// <param name="factory">An optional delegate returning an instance of a class deriving from
/// <see cref="ObservableTableViewSource{TItem}"/>. This can be used if you need to implement
/// specific features in addition to the built-in features of ObservableTableViewSource.</param>
/// <returns>The new instance of ObservableTableViewSource.</returns>
public static ObservableTableViewSource<TItem> GetTableViewSource<TItem>(
this ObservableCollection<TItem> list,
Action<UITableViewCell, TItem, NSIndexPath> bindCellDelegate,
string reuseId = null,
Func<ObservableTableViewSource<TItem>> factory = null)
{
if (factory != null)
{
var coll = factory();
coll.DataSource = list;
coll.BindCellDelegate = bindCellDelegate;
coll.ReuseId = reuseId;
return coll;
}
return new ObservableTableViewSource<TItem>
{
DataSource = list,
BindCellDelegate = bindCellDelegate,
ReuseId = reuseId
};
}
/// <summary>
/// Creates a new <see cref="ObservableTableViewSource{TItem}"/> for a given <see cref="IList{TItem}"/>.
/// Note that if the IList doesn't implement INotifyCollectionChanged, the associated UITableView won't be
/// updated when the IList changes.
/// </summary>
/// <typeparam name="TItem">The type of the items in the IList.</typeparam>
/// <param name="list">The IList that should be represented in the associated UITableView</param>
/// <param name="createCellDelegate">A delegate to a method creating or reusing a <see cref="UITableViewCell"/>.
/// The cell will then be passed to the bindCellDelegate delegate to set the elements' properties.
/// Use this method only if you don't want to register with the UITableView.RegisterClassForCellReuse method
/// for cell reuse.</param>
/// <param name="bindCellDelegate">A delegate to a method taking a <see cref="UITableViewCell"/>
/// and setting its elements' properties according to the item passed as second parameter.</param>
/// <param name="reuseId">An ID used for optimization and cell reuse.</param>
/// <param name="factory">An optional delegate returning an instance of a class deriving from
/// <see cref="ObservableTableViewSource{TItem}"/>. This can be used if you need to implement
/// specific features in addition to the built-in features of ObservableTableViewSource.</param>
/// <returns>The new instance of ObservableTableViewSource.</returns>
public static ObservableTableViewSource<TItem> GetTableViewSource<TItem>(
this IList<TItem> list,
Func<NSString, UITableViewCell> createCellDelegate,
Action<UITableViewCell, TItem, NSIndexPath> bindCellDelegate,
string reuseId = null,
Func<ObservableTableViewSource<TItem>> factory = null)
{
if (factory != null)
{
var coll = factory();
coll.DataSource = list;
coll.BindCellDelegate = bindCellDelegate;
coll.CreateCellDelegate = createCellDelegate;
coll.ReuseId = reuseId;
return coll;
}
return new ObservableTableViewSource<TItem>
{
DataSource = list,
BindCellDelegate = bindCellDelegate,
CreateCellDelegate = createCellDelegate,
ReuseId = reuseId
};
}
/// <summary>
/// Creates a new <see cref="ObservableTableViewSource{TItem}"/> for a given <see cref="ObservableCollection{TItem}"/>.
/// The associated UITableView will be updated when the ObservableCollection changes.
/// </summary>
/// <typeparam name="TItem">The type of the items in the IList.</typeparam>
/// <param name="list">The ObservableCollection that should be represented in the associated UITableView</param>
/// <param name="bindCellDelegate">A delegate to a method taking a <see cref="UITableViewCell"/>
/// and setting its elements' properties according to the item passed as second parameter.</param>
/// <param name="createCellDelegate">A delegate to a method creating or reusing a <see cref="UITableViewCell"/>.
/// The cell will then be passed to the bindCellDelegate delegate to set the elements' properties.
/// Use this method only if you don't want to register with the UITableView.RegisterClassForCellReuse method
/// for cell reuse.</param>
/// <param name="reuseId">An ID used for optimization and cell reuse.</param>
/// <param name="factory">An optional delegate returning an instance of a class deriving from
/// <see cref="ObservableTableViewSource{TItem}"/>. This can be used if you need to implement
/// specific features in addition to the built-in features of ObservableTableViewSource.</param>
/// <returns>The new instance of ObservableTableViewSource.</returns>
public static ObservableTableViewSource<TItem> GetTableViewSource<TItem>(
this ObservableCollection<TItem> list,
Func<NSString, UITableViewCell> createCellDelegate,
Action<UITableViewCell, TItem, NSIndexPath> bindCellDelegate,
string reuseId = null,
Func<ObservableTableViewSource<TItem>> factory = null)
{
if (factory != null)
{
var coll = factory();
coll.DataSource = list;
coll.BindCellDelegate = bindCellDelegate;
coll.CreateCellDelegate = createCellDelegate;
coll.ReuseId = reuseId;
return coll;
}
return new ObservableTableViewSource<TItem>
{
DataSource = list,
BindCellDelegate = bindCellDelegate,
CreateCellDelegate = createCellDelegate,
ReuseId = reuseId
};
}
internal static string GetDefaultEventNameForControl(this Type type)
{
string eventName = null;

Просмотреть файл

@ -0,0 +1,401 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using Foundation;
using UIKit;
namespace GalaSoft.MvvmLight.Helpers
{
/// <summary>
/// A <see cref="UICollectionViewSource"/> that automatically updates the associated <see cref="UICollectionView"/> when its
/// data source changes. Note that the changes are only observed if the data source
/// implements <see cref="INotifyCollectionChanged"/>.
/// </summary>
/// <typeparam name="TItem">The type of the items in the data source.</typeparam>
/// <typeparam name="TCell">The type of the <see cref="UICollectionViewCell"/> used in the CollectionView.
/// This can either be UICollectionViewCell or a derived type.</typeparam>
public class ObservableCollectionViewSource<TItem, TCell> : UICollectionViewSource, INotifyPropertyChanged
where TCell : UICollectionViewCell
{
/// <summary>
/// The <see cref="SelectedItem" /> property's name.
/// </summary>
public const string SelectedItemPropertyName = "SelectedItem";
private readonly NSString _defaultReuseId = new NSString("C");
private readonly Thread _mainThread;
private IList<TItem> _dataSource;
private INotifyCollectionChanged _notifier;
private NSString _reuseId;
private TItem _selectedItem;
private UICollectionView _view;
/// <summary>
/// A delegate to a method taking a <see cref="UICollectionViewCell"/>
/// and setting its elements' properties according to the item
/// passed as second parameter.
/// </summary>
public Action<TCell, TItem, NSIndexPath> BindCellDelegate
{
get;
set;
}
/// <summary>
/// The data source of this list controller.
/// </summary>
public IList<TItem> DataSource
{
get
{
return _dataSource;
}
set
{
if (Equals(_dataSource, value))
{
return;
}
if (_notifier != null)
{
_notifier.CollectionChanged -= HandleCollectionChanged;
}
_dataSource = value;
_notifier = value as INotifyCollectionChanged;
if (_notifier != null)
{
_notifier.CollectionChanged += HandleCollectionChanged;
}
if (_view != null)
{
_view.ReloadData();
}
}
}
/// <summary>
/// A delegate to a method returning a <see cref="UICollectionReusableView"/>
/// and used to set supplementary views on the UICollectionView.
/// </summary>
public Func<NSString, NSIndexPath, UICollectionReusableView> GetSupplementaryViewDelegate
{
get;
set;
}
/// <summary>
/// A reuse identifier for the UICollectionView's cells.
/// </summary>
public string ReuseId
{
get
{
return NsReuseId.ToString();
}
set
{
_reuseId = string.IsNullOrEmpty(value) ? null : new NSString(value);
}
}
/// <summary>
/// Gets the UICollectionView's selected item. You can use one-way databinding on this property.
/// </summary>
public TItem SelectedItem
{
get
{
return _selectedItem;
}
set
{
if (Equals(_selectedItem, value))
{
return;
}
_selectedItem = value;
RaisePropertyChanged(SelectedItemPropertyName);
RaiseSelectionChanged();
}
}
private NSString NsReuseId
{
get
{
return _reuseId ?? _defaultReuseId;
}
}
/// <summary>
/// Creates and initializes a new instance of <see cref="ObservableCollectionViewSource{TItem, TCell}"/>
/// </summary>
public ObservableCollectionViewSource()
{
_mainThread = Thread.CurrentThread;
}
/// <summary>
/// Overrides the <see cref="UICollectionViewSource.GetCell"/> method.
/// Creates and returns a cell for the UICollectionView. Where needed, this method will
/// optimize the reuse of cells for a better performance.
/// </summary>
/// <param name="collectionView">The UICollectionView associated to this source.</param>
/// <param name="indexPath">The NSIndexPath pointing to the item for which the cell must be returned.</param>
/// <returns>The created and initialised <see cref="UICollectionViewCell"/>.</returns>
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
var cell = (TCell)collectionView.DequeueReusableCell(NsReuseId, indexPath);
try
{
var coll = _dataSource;
if (coll != null)
{
var item = coll[indexPath.Row];
BindCell(cell, item, indexPath);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
return cell;
}
/// <summary>
/// Overrides the <see cref="UICollectionViewSource.GetItemsCount"/> method.
/// Gets the number of items in the data source.
/// </summary>
/// <param name="collectionView">The UICollectionView associated to this source.</param>
/// <param name="section">The section for which the count is needed. In the current
/// implementation, only one section is supported.</param>
/// <returns>The number of items in the data source.</returns>
public override nint GetItemsCount(UICollectionView collectionView, nint section)
{
SetView(collectionView);
return _dataSource.Count;
}
/// <summary>
/// Overrides the <see cref="UICollectionViewSource.GetViewForSupplementaryElement"/> method.
/// When called, checks if the <see cref="GetSupplementaryViewDelegate"/>
/// delegate has been set. If yes, calls that delegate to get a supplementary view for the UICollectionView.
/// </summary>
/// <param name="collectionView">The UICollectionView associated to this source.</param>
/// <param name="elementKind">The kind of supplementary element.</param>
/// <param name="indexPath">The NSIndexPath pointing to the element.</param>
/// <returns>A supplementary view for the UICollectionView.</returns>
public override UICollectionReusableView GetViewForSupplementaryElement(
UICollectionView collectionView,
NSString elementKind,
NSIndexPath indexPath)
{
if (GetSupplementaryViewDelegate == null)
{
throw new InvalidOperationException(
"GetViewForSupplementaryElement was called but no GetSupplementaryViewDelegate was found");
}
var view = GetSupplementaryViewDelegate(elementKind, indexPath);
return view;
}
/// <summary>
/// Overrides the <see cref="UICollectionViewSource.ItemDeselected"/> method.
/// Called when an item is deselected in the UICollectionView.
/// <remark>If you subclass ObservableCollectionViewSource, you may override this method
/// but you may NOT call base.ItemDeselected(...) in your overriden method, as this causes an exception
/// in iOS. Because of this, you must take care of resetting the <see cref="SelectedItem"/> property
/// yourself by calling SelectedItem = default(TItem);</remark>
/// </summary>
/// <param name="collectionView">The UICollectionView associated to this source.</param>
/// <param name="indexPath">The NSIndexPath pointing to the element.</param>
public override void ItemDeselected(UICollectionView collectionView, NSIndexPath indexPath)
{
SelectedItem = default(TItem);
}
/// <summary>
/// Overrides the <see cref="UICollectionViewSource.ItemSelected"/> method.
/// Called when an item is selected in the UICollectionView.
/// <remark>If you subclass ObservableCollectionViewSource, you may override this method
/// but you may NOT call base.ItemSelected(...) in your overriden method, as this causes an exception
/// in iOS. Because of this, you must take care of setting the <see cref="SelectedItem"/> property
/// yourself by calling var item = GetItem(indexPath); SelectedItem = item;</remark>
/// </summary>
/// <param name="collectionView">The UICollectionView associated to this source.</param>
/// <param name="indexPath">The NSIndexPath pointing to the element.</param>
public override void ItemSelected(UICollectionView collectionView, NSIndexPath indexPath)
{
var item = _dataSource[indexPath.Row];
SelectedItem = item;
}
/// <summary>
/// Overrides the <see cref="UICollectionViewSource.NumberOfSections"/> method.
/// The number of sections in this UICollectionView. In the current implementation,
/// only one section is supported.
/// </summary>
/// <param name="collectionView">The UICollectionView associated to this source.</param>
/// <returns></returns>
public override nint NumberOfSections(UICollectionView collectionView)
{
SetView(collectionView);
return 1;
}
/// <summary>
/// Sets a <see cref="UICollectionViewCell"/>'s elements according to an item's properties.
/// If a <see cref="BindCellDelegate"/> is available, this delegate will be used.
/// If not, a simple text will be shown.
/// </summary>
/// <param name="cell">The cell that will be prepared.</param>
/// <param name="item">The item that should be used to set the cell up.</param>
/// <param name="indexPath">The <see cref="NSIndexPath"/> for this cell.</param>
protected virtual void BindCell(UICollectionViewCell cell, object item, NSIndexPath indexPath)
{
if (BindCellDelegate == null)
{
throw new InvalidOperationException(
"BindCell was called but no BindCellDelegate was found");
}
BindCellDelegate((TCell)cell, (TItem)item, indexPath);
}
/// <summary>
/// Gets the item selected by the NSIndexPath passed as parameter.
/// </summary>
/// <param name="indexPath">The NSIndexPath pointing to the desired item.</param>
/// <returns>The item selected by the NSIndexPath passed as parameter.</returns>
protected TItem GetItem(NSIndexPath indexPath)
{
return _dataSource[indexPath.Row];
}
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event.
/// </summary>
/// <param name="propertyName">The name of the property that changed.</param>
protected virtual void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
private void HandleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_view == null)
{
return;
}
Action act = () =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
var count = e.NewItems.Count;
var paths = new NSIndexPath[count];
for (var i = 0; i < count; i++)
{
paths[i] = NSIndexPath.FromRowSection(e.NewStartingIndex + i, 0);
}
_view.InsertItems(paths);
}
break;
case NotifyCollectionChangedAction.Remove:
{
var count = e.OldItems.Count;
var paths = new NSIndexPath[count];
for (var i = 0; i < count; i++)
{
var index = NSIndexPath.FromRowSection(e.OldStartingIndex + i, 0);
paths[i] = index;
var item = e.OldItems[i];
if (Equals(SelectedItem, item))
{
SelectedItem = default(TItem);
}
}
_view.DeleteItems(paths);
}
break;
default:
_view.ReloadData();
break;
}
};
var isMainThread = Thread.CurrentThread == _mainThread;
if (isMainThread)
{
act();
}
else
{
NSOperationQueue.MainQueue.AddOperation(act);
NSOperationQueue.MainQueue.WaitUntilAllOperationsAreFinished();
}
}
private void RaiseSelectionChanged()
{
var handler = SelectionChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
private void SetView(UICollectionView collectionView)
{
if (_view != null)
{
return;
}
_view = collectionView;
_view.RegisterClassForCell(typeof (TCell), NsReuseId);
}
/// <summary>
/// Occurs when a property of this instance changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Occurs when a new item gets selected in the UICollectionView.
/// </summary>
public event EventHandler SelectionChanged;
}
}

Просмотреть файл

@ -0,0 +1,512 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using Foundation;
using UIKit;
namespace GalaSoft.MvvmLight.Helpers
{
/// <summary>
/// A <see cref="UITableViewSource"/> that automatically updates the associated <see cref="UITableView"/> when its
/// data source changes. Note that the changes are only observed if the data source
/// implements <see cref="INotifyCollectionChanged"/>.
/// </summary>
/// <typeparam name="TItem">The type of the items in the data source.</typeparam>
public class ObservableTableViewSource<TItem> : UITableViewSource, INotifyPropertyChanged
{
/// <summary>
/// The <see cref="SelectedItem" /> property's name.
/// </summary>
public const string SelectedItemPropertyName = "SelectedItem";
private readonly NSString _defaultReuseId = new NSString("C");
private readonly Thread _mainThread;
private IList<TItem> _dataSource;
private INotifyCollectionChanged _notifier;
private NSString _reuseId;
private TItem _selectedItem;
private UITableView _view;
/// <summary>
/// When set, specifies which animation should be used when rows are added.
/// </summary>
public UITableViewRowAnimation AddAnimation
{
get;
set;
}
/// <summary>
/// A delegate to a method taking a <see cref="UITableViewCell"/>
/// and setting its elements' properties according to the item
/// passed as second parameter.
/// </summary>
public Action<UITableViewCell, TItem, NSIndexPath> BindCellDelegate
{
get;
set;
}
/// <summary>
/// A delegate to a method creating or reusing a <see cref="UITableViewCell"/>.
/// The cell will then be passed to the <see cref="BindCellDelegate"/>
/// delegate to set the elements' properties. Note that this delegate is only
/// used if you didn't register with a ReuseID using the UITableView.RegisterClassForCell method.
/// </summary>
public Func<NSString, UITableViewCell> CreateCellDelegate
{
get;
set;
}
/// <summary>
/// The data source of this list controller.
/// </summary>
public IList<TItem> DataSource
{
get
{
return _dataSource;
}
set
{
if (Equals(_dataSource, value))
{
return;
}
if (_notifier != null)
{
_notifier.CollectionChanged -= HandleCollectionChanged;
}
_dataSource = value;
_notifier = value as INotifyCollectionChanged;
if (_notifier != null)
{
_notifier.CollectionChanged += HandleCollectionChanged;
}
if (_view != null)
{
_view.ReloadData();
}
}
}
/// <summary>
/// When set, specifieds which animation should be used when a row is deleted.
/// </summary>
public UITableViewRowAnimation DeleteAnimation
{
get;
set;
}
/// <summary>
/// When set, returns the height of the view that will be used for the TableView's footer.
/// </summary>
/// <seealso cref="GetViewForFooterDelegate"/>
public Func<nfloat> GetHeightForFooterDelegate
{
get;
set;
}
/// <summary>
/// When set, returns the height of the view that will be used for the TableView's header.
/// </summary>
/// <seealso cref="GetViewForHeaderDelegate"/>
public Func<nfloat> GetHeightForHeaderDelegate
{
get;
set;
}
/// <summary>
/// When set, returns a view that can be used as the TableView's footer.
/// </summary>
/// <seealso cref="GetHeightForFooterDelegate"/>
public Func<UIView> GetViewForFooterDelegate
{
get;
set;
}
/// <summary>
/// When set, returns a view that can be used as the TableView's header.
/// </summary>
/// <seealso cref="GetHeightForHeaderDelegate"/>
public Func<UIView> GetViewForHeaderDelegate
{
get;
set;
}
/// <summary>
/// A reuse identifier for the TableView's cells.
/// </summary>
public string ReuseId
{
get
{
return NsReuseId.ToString();
}
set
{
_reuseId = string.IsNullOrEmpty(value) ? null : new NSString(value);
}
}
/// <summary>
/// Gets the UITableView's selected item. You can use one-way databinding on this property.
/// </summary>
public TItem SelectedItem
{
get
{
return _selectedItem;
}
set
{
if (Equals(_selectedItem, value))
{
return;
}
_selectedItem = value;
RaisePropertyChanged(SelectedItemPropertyName);
RaiseSelectionChanged();
}
}
private NSString NsReuseId
{
get
{
return _reuseId ?? _defaultReuseId;
}
}
/// <summary>
/// Constructs and initializes an instance of <see cref="ObservableTableViewSource{TItem}"/>
/// </summary>
public ObservableTableViewSource()
{
_mainThread = Thread.CurrentThread;
AddAnimation = UITableViewRowAnimation.Automatic;
DeleteAnimation = UITableViewRowAnimation.Automatic;
}
/// <summary>
/// Creates and returns a cell for the UITableView. Where needed, this method will
/// optimize the reuse of cells for a better performance.
/// </summary>
/// <param name="view">The UITableView associated to this source.</param>
/// <param name="indexPath">The NSIndexPath pointing to the item for which the cell must be returned.</param>
/// <returns>The created and initialised <see cref="UITableViewCell"/>.</returns>
public override UITableViewCell GetCell(UITableView view, NSIndexPath indexPath)
{
if (_view == null)
{
_view = view;
}
var cell = view.DequeueReusableCell(NsReuseId) ?? CreateCell(NsReuseId);
try
{
var coll = _dataSource;
if (coll != null)
{
var item = coll[indexPath.Row];
BindCell(cell, item, indexPath);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
return cell;
}
/// <summary>
/// When called, checks if the <see cref="GetHeightForFooterDelegate"/>has been set.
/// If yes, calls that delegate to get the TableView's footer height.
/// </summary>
/// <param name="tableView">The active TableView.</param>
/// <param name="section">The section index.</param>
/// <returns>The footer's height.</returns>
/// <remarks>In the current implementation, only one section is supported.</remarks>
public override nfloat GetHeightForFooter(UITableView tableView, nint section)
{
if (GetHeightForFooterDelegate != null)
{
return GetHeightForFooterDelegate();
}
return 0;
}
/// <summary>
/// When called, checks if the <see cref="GetHeightForHeaderDelegate"/>
/// delegate has been set. If yes, calls that delegate to get the TableView's header height.
/// </summary>
/// <param name="tableView">The active TableView.</param>
/// <param name="section">The section index.</param>
/// <returns>The header's height.</returns>
/// <remarks>In the current implementation, only one section is supported.</remarks>
public override nfloat GetHeightForHeader(UITableView tableView, nint section)
{
if (GetHeightForHeaderDelegate != null)
{
return GetHeightForHeaderDelegate();
}
return 0;
}
/// <summary>
/// When called, checks if the <see cref="GetViewForFooterDelegate"/>
/// delegate has been set. If yes, calls that delegate to get the TableView's footer.
/// </summary>
/// <param name="tableView">The active TableView.</param>
/// <param name="section">The section index.</param>
/// <returns>The UIView that should appear as the section's footer.</returns>
/// <remarks>In the current implementation, only one section is supported.</remarks>
public override UIView GetViewForFooter(UITableView tableView, nint section)
{
if (GetViewForFooterDelegate != null)
{
return GetViewForFooterDelegate();
}
return base.GetViewForFooter(tableView, section);
}
/// <summary>
/// When called, checks if the <see cref="GetViewForHeaderDelegate"/>
/// delegate has been set. If yes, calls that delegate to get the TableView's header.
/// </summary>
/// <param name="tableView">The active TableView.</param>
/// <param name="section">The section index.</param>
/// <returns>The UIView that should appear as the section's header.</returns>
/// <remarks>In the current implementation, only one section is supported.</remarks>
public override UIView GetViewForHeader(UITableView tableView, nint section)
{
if (GetViewForHeaderDelegate != null)
{
return GetViewForHeaderDelegate();
}
return base.GetViewForHeader(tableView, section);
}
/// <summary>
/// Overrides the <see cref="UITableViewSource.NumberOfSections"/> method.
/// </summary>
/// <param name="tableView">The active TableView.</param>
/// <returns>The number of sections of the UITableView.</returns>
/// <remarks>In the current implementation, only one section is supported.</remarks>
public override nint NumberOfSections(UITableView tableView)
{
return 1;
}
/// <summary>
/// Overrides the <see cref="UITableViewSource.RowDeselected"/> method. When called, sets the
/// <see cref="SelectedItem"/> property to null and raises the PropertyChanged and the SelectionChanged events.
/// </summary>
/// <param name="tableView">The active TableView.</param>
/// <param name="indexPath">The row's NSIndexPath.</param>
public override void RowDeselected(UITableView tableView, NSIndexPath indexPath)
{
SelectedItem = default(TItem);
}
/// <summary>
/// Overrides the <see cref="UITableViewSource.RowSelected"/> method. When called, sets the
/// <see cref="SelectedItem"/> property and raises the PropertyChanged and the SelectionChanged events.
/// </summary>
/// <param name="tableView">The active TableView.</param>
/// <param name="indexPath">The row's NSIndexPath.</param>
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
var item = _dataSource != null ? _dataSource[indexPath.Row] : default(TItem);
SelectedItem = item;
}
/// <summary>
/// Overrides the <see cref="UITableViewSource.RowsInSection"/> method
/// and returns the number of rows in the associated data source.
/// </summary>
/// <param name="tableView">The active TableView.</param>
/// <param name="section">The active section.</param>
/// <returns>The number of rows in the data source.</returns>
/// <remarks>In the current implementation, only one section is supported.</remarks>
public override nint RowsInSection(UITableView tableView, nint section)
{
if (_view == null)
{
_view = tableView;
}
return _dataSource == null ? 0 : _dataSource.Count;
}
/// <summary>
/// Binds a <see cref="UITableViewCell"/> to an item's properties.
/// If a <see cref="BindCellDelegate"/> is available, this delegate will be used.
/// If not, a simple text will be shown.
/// </summary>
/// <param name="cell">The cell that will be prepared.</param>
/// <param name="item">The item that should be used to set the cell up.</param>
/// <param name="indexPath">The <see cref="NSIndexPath"/> for this cell.</param>
protected virtual void BindCell(UITableViewCell cell, object item, NSIndexPath indexPath)
{
if (BindCellDelegate == null)
{
cell.TextLabel.Text = item.ToString();
}
else
{
BindCellDelegate(cell, (TItem)item, indexPath);
}
}
/// <summary>
/// Creates a <see cref="UITableViewCell"/> corresponding to the reuseId.
/// If it is set, the <see cref="CreateCellDelegate"/> delegate will be used.
/// </summary>
/// <param name="reuseId">A reuse identifier for the cell.</param>
/// <returns>The created cell.</returns>
protected virtual UITableViewCell CreateCell(NSString reuseId)
{
if (CreateCellDelegate == null)
{
return new UITableViewCell(UITableViewCellStyle.Default, reuseId);
}
return CreateCellDelegate(reuseId);
}
/// <summary>
/// Gets the item selected by the NSIndexPath passed as parameter.
/// </summary>
/// <param name="indexPath">The NSIndexPath pointing to the desired item.</param>
/// <returns>The item selected by the NSIndexPath passed as parameter.</returns>
protected TItem GetItem(NSIndexPath indexPath)
{
return _dataSource[indexPath.Row];
}
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event.
/// </summary>
/// <param name="propertyName">The name of the property that changed.</param>
protected virtual void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
private void HandleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_view == null)
{
return;
}
Action act = () =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
var count = e.NewItems.Count;
var paths = new NSIndexPath[count];
for (var i = 0; i < count; i++)
{
paths[i] = NSIndexPath.FromRowSection(e.NewStartingIndex + i, 0);
}
_view.InsertRows(paths, AddAnimation);
}
break;
case NotifyCollectionChangedAction.Remove:
{
var count = e.OldItems.Count;
var paths = new NSIndexPath[count];
for (var i = 0; i < count; i++)
{
var index = NSIndexPath.FromRowSection(e.OldStartingIndex + i, 0);
paths[i] = index;
var item = e.OldItems[i];
if (Equals(SelectedItem, item))
{
SelectedItem = default(TItem);
}
}
_view.DeleteRows(paths, DeleteAnimation);
}
break;
default:
_view.ReloadData();
break;
}
};
var isMainThread = Thread.CurrentThread == _mainThread;
if (isMainThread)
{
act();
}
else
{
NSOperationQueue.MainQueue.AddOperation(act);
NSOperationQueue.MainQueue.WaitUntilAllOperationsAreFinished();
}
}
private void RaiseSelectionChanged()
{
var handler = SelectionChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
/// <summary>
/// Occurs when a property of this instance changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Occurs when a new item gets selected in the list.
/// </summary>
public event EventHandler SelectionChanged;
}
}

Просмотреть файл

@ -117,9 +117,15 @@
<Compile Include="..\..\GalaSoft.MvvmLight.Platform %28iOS%29\Helpers\ExtensionsApple.cs">
<Link>Helpers\ExtensionsApple.cs</Link>
</Compile>
<Compile Include="..\..\GalaSoft.MvvmLight.Platform %28iOS%29\Helpers\ObservableCollectionViewSource.cs">
<Link>Helpers\ObservableCollectionViewSource.cs</Link>
</Compile>
<Compile Include="..\..\GalaSoft.MvvmLight.Platform %28iOS%29\Helpers\ObservableTableViewController.cs">
<Link>Helpers\ObservableTableViewController.cs</Link>
</Compile>
<Compile Include="..\..\GalaSoft.MvvmLight.Platform %28iOS%29\Helpers\ObservableTableViewSource.cs">
<Link>Binding\ObservableTableViewSource.cs</Link>
</Compile>
<Compile Include="..\AndroidTestApp\Binding\BindingAccountTest.cs">
<Link>Binding\BindingAccountTest.cs</Link>
</Compile>
@ -191,6 +197,7 @@
<Compile Include="AppDelegate.cs" />
<None Include="GettingStarted.Xamarin" />
<None Include="Info.plist" />
<Compile Include="ObservableTable\ObservableTableViewSourceTest.cs" />
<Compile Include="ObservableTable\ReuseIdTest.cs" />
<Compile Include="ObservableTable\TestUiTableViewCell.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />

Просмотреть файл

@ -0,0 +1,205 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Foundation;
using GalaSoft.MvvmLight.Helpers;
using GalaSoft.MvvmLight.Test.ViewModel;
using NUnit.Framework;
using UIKit;
namespace GalaSoft.MvvmLight.Test.ObservableTable
{
[TestFixture]
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class ObservableTableViewSourceTest
{
private UITableView _tableView;
public TestViewModel Vm
{
get;
private set;
}
[Test]
public void ObservableTableViewSource_GetSourceWithCollectionCheckingItemCount()
{
Vm = GetViewModel();
_tableView = new UITableView();
var source = Vm.ItemsCollection.GetTableViewSource(
CreateCell,
BindCell);
_tableView.Source = source;
Assert.AreEqual(
Vm.ItemsCollection.Count,
_tableView.NumberOfRowsInSection(0));
Vm.ItemsCollection.Add(new TestItem("New one"));
Assert.AreEqual(
Vm.ItemsCollection.Count,
_tableView.NumberOfRowsInSection(0));
}
[Test]
public void ObservableTableViewSource_GetSourceWithCollectionGettingLastItem()
{
Vm = new TestViewModel
{
ItemsCollection = new ObservableCollection<TestItem>()
};
_tableView = new UITableView();
var source = Vm.ItemsCollection.GetTableViewSource(
CreateCell,
BindCell);
_tableView.Source = source;
Assert.AreEqual(
Vm.ItemsCollection.Count,
_tableView.NumberOfRowsInSection(0));
// In unit tests, GetCell only gets called for the first inserted row (no layout pass)
Vm.ItemsCollection.Add(new TestItem("One"));
var lastCell = _tableView.CellAt(Vm.ItemsCollection.First().RowIndexPath);
Assert.AreEqual(
Vm.ItemsCollection.Last().Title,
lastCell.TextLabel.Text);
}
[Test]
public void ObservableTableViewSource_GetSourceWithListCheckingItemCount()
{
Vm = GetViewModel();
_tableView = new UITableView();
var source = Vm.ItemsList.GetTableViewSource(
CreateCell,
BindCell);
_tableView.Source = source;
Assert.AreEqual(
Vm.ItemsList.Count,
_tableView.NumberOfRowsInSection(0));
}
[Test]
public void ObservableTableViewSource_NewSourceWithCollectionCheckingItemCount()
{
Vm = GetViewModel();
_tableView = new UITableView();
var source = new ObservableTableViewSource<TestItem>
{
CreateCellDelegate = CreateCell,
BindCellDelegate = BindCell,
DataSource = Vm.ItemsCollection,
};
_tableView.Source = source;
Assert.AreEqual(
Vm.ItemsCollection.Count,
_tableView.NumberOfRowsInSection(0));
Vm.ItemsCollection.Add(new TestItem("New one"));
Assert.AreEqual(
Vm.ItemsCollection.Count,
_tableView.NumberOfRowsInSection(0));
}
[Test]
public void ObservableTableViewSource_NewSourceWithCollectionGettingLastItem()
{
Vm = new TestViewModel
{
ItemsCollection = new ObservableCollection<TestItem>()
};
_tableView = new UITableView();
var source = new ObservableTableViewSource<TestItem>
{
CreateCellDelegate = CreateCell,
BindCellDelegate = BindCell,
DataSource = Vm.ItemsCollection,
};
_tableView.Source = source;
Assert.AreEqual(
Vm.ItemsCollection.Count,
_tableView.NumberOfRowsInSection(0));
// In unit tests, GetCell only gets called for the first inserted row (no layout pass)
Vm.ItemsCollection.Add(new TestItem("One"));
var lastCell = _tableView.CellAt(Vm.ItemsCollection.First().RowIndexPath);
Assert.AreEqual(
Vm.ItemsCollection.Last().Title,
lastCell.TextLabel.Text);
}
[Test]
public void ObservableTableViewSource_NewSourceWithListCheckingItemCount()
{
Vm = GetViewModel();
_tableView = new UITableView();
var source = new ObservableTableViewSource<TestItem>
{
CreateCellDelegate = CreateCell,
BindCellDelegate = BindCell,
DataSource = Vm.ItemsList,
};
_tableView.Source = source;
Assert.AreEqual(
Vm.ItemsList.Count,
_tableView.NumberOfRowsInSection(0));
}
private void BindCell(UITableViewCell cell, TestItem item, NSIndexPath path)
{
item.RowIndexPath = path;
cell.TextLabel.Text = item.Title;
}
private UITableViewCell CreateCell(NSString reuseId)
{
return new UITableViewCell(UITableViewCellStyle.Default, reuseId);
}
private TestViewModel GetViewModel()
{
return new TestViewModel
{
ItemsCollection = new ObservableCollection<TestItem>
{
new TestItem("123"),
new TestItem("234"),
new TestItem("345"),
},
ItemsList = new List<TestItem>
{
new TestItem("123"),
new TestItem("234"),
new TestItem("345"),
new TestItem("456"),
}
};
}
}
}