CardSafe/EはMVVMパターンに則って作るように心掛けたのですが、WPFと同じ感覚でSilverlightにMVVMパターンを適用しようとすると、色々と不都合が出てきて苦労させられました。Silverlightではオミットされている機能が結構あるので、そこに引っかかるとしばしば手戻りが発生します。このエントリではCardSafe/Eを書いていて気付いた点をとりあえず二点ほど。

SilverlightにはDataTemplate.Datatypeが存在しない

地味に痛いですこの仕様。ItemsControlから派生したコントロール(ListBoxなど)のItemをItemsSourceにセットしたINotifyPoropertyChangedを実装したコレクションから生成する際に、コレクションが列挙するインスタンスの型によってDataTemplateを切り替える場合、WPFではDataTemplate.Datatypeがあるので重宝するのですが、Silverlightでは3 RTWでもサポートされていません。DataTemplateSelectorもサポートされていないので、インスタンスの型や状態によってDataTemplateを切り替えることはできないようです。とりあえず逃げ道として自分はDataContextにセットされたインスタンスをみてContentを書き換えるロジックを書いたUserControlを用意しているのですが、面倒ですね。XAMLで宣言的に書きたいのでDataTemplate.Datatypeは是非サポートしてもらいたいものです。

TabcotrolのItemsSourceはTabItemのコレクションしか受け付けない

なんでー!?これまたあんまりな仕様。WPFではItemsControlから派生したコントロールのItemsSourceにINotifyPoropertyChangedを実装したコレクションをセットすると、その列挙する内容をListBoxItemやTabItemなど適当なコンテナに包んで表示してくれるので、あとはDataTemplateで表示内容を好きなようにカスタマイズしてやればよかったのですが、SilverlightのTabControlのItemsSourceはTabItemのコレクションしか受けてつけてくれないという。。これではTabItemに対応するViewModelのインスタンスをセットし、DataTemplateでViewを設定して表示内容をカスタマイズするというM-V-VMでよく知られたパターンが使えません。SilverlightでもListBoxではListBoxItem以外も受け付けてくれたのですけどねぇ。。ではどうするか。自分の場合はViewModelのコレクションをラップしてTabItemのコレクションとして公開するラッパークラスとConverterを用意することで凌ぎました。

ラッパークラスはこんな感じです。ViewModelのコレクションとViewModelからTabItemへの変換を行うFuncをコンストラクタの引数として取ります。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows.Controls;

namespace SharpLab.CredentialsLockerSilverlight.ViewModels {
	public class ReadOnlyObservableTabItemCollection<TViewModel, TViewModelCollection> : ICollection<TabItem>, INotifyCollectionChanged where TViewModelCollection : ICollection<TViewModel>, INotifyCollectionChanged {

		private TViewModelCollection _viewModels;
		private ObservableCollection<TabItem> _tabItems;
		private Func<TViewModel, TabItem> _converter;

		public ReadOnlyObservableTabItemCollection(TViewModelCollection viewModels, Func<TViewModel, TabItem> converter) {
			_converter = converter;
			_viewModels = viewModels;
			_tabItems = new ObservableCollection<TabItem>();

			foreach (var item in _viewModels) {
				_tabItems.Add(_converter(item));
			}

			_viewModels.CollectionChanged += new NotifyCollectionChangedEventHandler(_viewModels_CollectionChanged);

			_tabItems.CollectionChanged += new NotifyCollectionChangedEventHandler(_tabItems_CollectionChanged);
		}

		void _viewModels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
			switch (e.Action) {
				case NotifyCollectionChangedAction.Add:
					_tabItems.Insert(e.NewStartingIndex, _converter((TViewModel)e.NewItems[0]));
					break;
				case NotifyCollectionChangedAction.Remove:
					_tabItems.RemoveAt(e.OldStartingIndex);
					break;
				case NotifyCollectionChangedAction.Replace:
					_tabItems.RemoveAt(e.OldStartingIndex);
					_tabItems.Insert(e.NewStartingIndex, _converter((TViewModel)e.NewItems[0]));
					break;
				case NotifyCollectionChangedAction.Reset:
					_tabItems.Clear();
					foreach (var item in _viewModels) {
						_tabItems.Add(_converter(item));
					}
					break;
			}
		}

		void _tabItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
			if (CollectionChanged != null) {
				CollectionChanged(this, e);
			}
		}


		public event NotifyCollectionChangedEventHandler CollectionChanged;

		#region ICollection<TabItem> メンバ

		public void Add(TabItem item) {
			throw new NotSupportedException();
		}

		public void Clear() {
			throw new NotSupportedException();
		}

		public bool Contains(TabItem item) {
			throw new NotImplementedException();
		}

		public void CopyTo(TabItem[] array, int arrayIndex) {
			throw new NotSupportedException();
		}

		public int Count {
			get {
				throw new NotImplementedException();
			}
		}

		public bool IsReadOnly {
			get {
				return true;
			}
		}

		public bool Remove(TabItem item) {
			throw new NotSupportedException();
		}

		#endregion

		#region IEnumerable<TabItem> メンバ

		public IEnumerator<TabItem> GetEnumerator() {
			foreach (var item in _tabItems) {
				yield return item;
			}
		}

		#endregion

		#region IEnumerable メンバ

		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
			return GetEnumerator();
		}

		#endregion
	}
}

これを以下のようなConverterから利用します。

using System;
using System.Collections.ObjectModel;
using System.Windows.Controls;
using System.Windows.Data;
using SharpLab.CredentialsLockerSilverlight.ViewModels;

namespace SharpLab.CredentialsLockerSilverlight.Views.Converter {
	public class CardVMCol2TabItemColConverter : IValueConverter{
		#region IValueConverter メンバ

		public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
			return new ReadOnlyObservableTabItemCollection<CardViewModel, ObservableCollection<CardViewModel>>((ObservableCollection<CardViewModel>)value,
				(each) => {
					var newTabItem = new TabItem() {
						DataContext = each,
						Header = new CardTabHeaderView(),
						Content = new SharpLab.CredentialsLockerSilverlight.Views.CardView()
					};
					return newTabItem;

				});
		}

		public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
			throw new NotImplementedException();
		}

		#endregion
	}
}

こうしてやることでTabControlのItemsSourceにViewModelのコレクションをバインドできるようになります。Converterは無理を通すのに便利な機能ですね。。

 ItemsSource="{Binding Path=CardViewModels, Converter={StaticResource CardVMCol2TabItemColConverter}}"

Comments :1

kazuto 09-07-31 18:03:22 UTC

はじめまして。kazutoと言います。
いつも拝見させて頂いております。とても良い情報ばかりでいつも楽しみにしています。

今回の無いようにて、DataTemplate.DataTypeがサポートされていない事によってUserControlに回避処理を実装されているとの事ですが、どのように実装されているのでしょうか?

私もWPFと同様な機能をSilverlightでも実装したく、色々とやっているのですがDataContextに設定された物を解析し、Contentに設定する部分に凄く興味が沸きコメントさせて頂きました。

SilverlightではDataContextChangedがサポートされていないので、どのタイミングでDataContextの設定を監視しようか未だ実装が行えておりません。
やはり実装アプリに依存した形でした実装はきついのでしょうか?

DataTypeのサポート本当にして頂きたいですね。

では、失礼します。