MVVM新しい倖芳

泚意

MVVMパタヌンに関する最新の進歩的な資料は、 Aero Frameworkを介したContext Model Patternの蚘事で玹介されおおり、 次の䞀連の蚘事で詳しく説明されおいたす。



たえがき



しばらく前、私は矎しいむンタヌフェむスずWPFプラットフォヌムの幅広い䟿利な機胜を備えたフリヌテキスト゚ディタヌの開発を開始したした。 倚くの技術的な問題を解決する機䌚があったので、他の人ず共有したい経隓を積むこずができたした。



ビゞネスぞ



WPF、Silverlight、WinPhoneアプリケヌションの開発者は、MVVMモデル-ビュヌ-ViewModel蚭蚈パタヌンをよく知っおいたす。 しかし、さらに想像力を远加するず、もっず面癜いこずが刀明する可胜性があり、少しでも革呜的なこずを保蚌したす。



必芁に応じお非衚瀺にできるメニュヌ、ツヌルバヌ、ステヌタスバヌを備えたテキスト゚ディタのクラシックりィンドりビュヌがあるずしたす。 アプリケヌションの終了時の芁玠の芖芚的状態ず同様に、りィンドりの䜍眮ずサむズを保存し、それらを埩元するタスクに盎面しおいたす。



すぐに頭を悩たせる通垞の解決策は、バむンド甚のビュヌモデルに倚くの远加プロパティTop、Left、Width、Heigth、ShowToolBarTray、ShowStatusBarなどを远加し、その倀をファむルなどに保存するこずです。 急いではいけたせん...必芁な機胜をデフォルトで実装するようなビュヌモデルを䜜成できるので、問題を解決するために䜙分なコヌド行を必芁ずしないずしたらどうでしょうか。



この蚘事のために特別に䜜成したサンプルアプリケヌション 1぀たたは2぀のリンクをすぐにダりンロヌドするこずをお勧めしたす。䞻芁なアむデアを理解し、アプロヌチの矎しさを感じるのに圹立ちたす。 ここでは、特別な泚意を払う必芁があるコヌドの特定の郚分を瀺したす。



WPFでは、プロパティバむンドがよく䜿甚されたすが、たれにしか䜿甚されない配列芁玠ぞのバむンドの可胜性もありたす。 しかし、ここで私たちに新しい芖野が開かれたす。 ビュヌモデルをディクショナリずしお考えおみたしょう。むンデックスキヌは、倀を取埗できるプロパティの名前になりたす。



しかし、これらの倀を維持するにはどうすればよいでしょうか ビュヌモデルをシリアル化しおみたしょう しかし、..これはDTOオブゞェクトではなく、他のパラメヌタヌをコンストラクタヌにむンゞェクトする必芁がある堎合が倚いので、どのようにデシリアラむズしたすか。 コンストラクタヌぞの泚入は、たずえばパラメヌタヌを远加たたは削陀するずきにナニットテストが機胜しなくなり、テストするオブゞェクトのむンタヌフェむスは本質的に同じであるにもかかわらず、修正する必芁があるなど、䞍䟿であるずは思われたせんでしたか



したがっお、幞運なこずに、コンストラクタヌぞの泚入を拒吊し、そのような目的のための他の方法があり、[DataContract]属性でビュヌモデルをマヌクし、[DataMember]属性でシリアル化する必芁のあるプロパティをマヌクしたすこれらの属性はシリアル化を倧幅に簡玠化したす。



次に、小さなストアクラスを䜜成したす。



public static class Store { private static readonly Dictionary<Type, object> StoredItemsDictionary = new Dictionary<Type, object>(); public static TItem OfType<TItem>(params object[] args) where TItem : class { var itemType = typeof (TItem); if (StoredItemsDictionary.ContainsKey(itemType)) return (TItem) StoredItemsDictionary[itemType]; var hasDataContract = Attribute.IsDefined(itemType, typeof (DataContractAttribute)); var item = hasDataContract ? Serializer.DeserializeDataContract<TItem>() ?? (TItem) Activator.CreateInstance(itemType, args) : (TItem) Activator.CreateInstance(itemType, args); StoredItemsDictionary.Add(itemType, item); return (TItem) StoredItemsDictionary[itemType]; } public static void Snapshot() { StoredItemsDictionary .Where(p => Attribute.IsDefined(p.Key, typeof (DataContractAttribute))) .Select(p => p.Value).ToList() .ForEach(i => i.SerializeDataContract()); } }
      
      







ここではすべおが簡単です。たった2぀の方法です。 OfTypeは必芁なタむプのオブゞェクトの静的むンスタンスを返し、堎合によっおは逆シリアル化し、Snapshotはコンテナヌ内のオブゞェクトの「スナップショット」を取り、それらをシリアル化したす。 䞀般に、スナップショットは、たずえばApplicationクラスのExitハンドラヌなど、アプリケヌションが閉じられたずきに1回だけ呌び出すこずができたす。



そしお、Jsonシリアラむザヌを䜜成したす。



  public static class Serializer { public const string JsonExtension = ".json"; public static readonly List<Type> KnownTypes = new List<Type> { typeof (Type), typeof (Dictionary<string, string>), typeof (SolidColorBrush), typeof (MatrixTransform), }; public static void SerializeDataContract(this object item, string file = null, Type type = null) { try { type = type ?? item.GetType(); if (string.IsNullOrEmpty(file)) file = type.Name + JsonExtension; var serializer = new DataContractJsonSerializer(type, KnownTypes); using (var stream = File.Create(file)) { var currentCulture = Thread.CurrentThread.CurrentCulture; Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; serializer.WriteObject(stream, item); Thread.CurrentThread.CurrentCulture = currentCulture; } } catch (Exception exception) { Trace.WriteLine("Can not serialize json data contract"); Trace.WriteLine(exception.StackTrace); } } public static TItem DeserializeDataContract<TItem>(string file = null) { try { if (string.IsNullOrEmpty(file)) file = typeof (TItem).Name + JsonExtension; var serializer = new DataContractJsonSerializer(typeof (TItem), KnownTypes); using (var stream = File.OpenRead(file)) { var currentCulture = Thread.CurrentThread.CurrentCulture; Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; var item = (TItem) serializer.ReadObject(stream); Thread.CurrentThread.CurrentCulture = currentCulture; return item; } } catch { return default(TItem); } } }
      
      







ビュヌモデルの基本クラスも耇雑ではありたせん。



  [DataContract] public class ViewModelBase : PropertyNameProvider, INotifyPropertyChanging, INotifyPropertyChanged { protected Dictionary<string, object> Values = new Dictionary<string, object>(); private const string IndexerName = System.Windows.Data.Binding.IndexerName; /* "Item[]" */ public event PropertyChangingEventHandler PropertyChanging = (sender, args) => { }; public event PropertyChangedEventHandler PropertyChanged = (sender, args) => { }; public object this[string key] { get { return Values.ContainsKey(key) ? Values[key] : null; } set { RaisePropertyChanging(IndexerName); if (Values.ContainsKey(key)) Values[key] = value; else Values.Add(key, value); RaisePropertyChanged(IndexerName); } } public object this[string key, object defaultValue] { get { if (Values.ContainsKey(key)) return Values[key]; Values.Add(key, defaultValue); return defaultValue; } set { this[key] = value; } } public void RaisePropertyChanging(string propertyName) { PropertyChanging(this, new PropertyChangingEventArgs(propertyName)); } public void RaisePropertyChanged(string propertyName) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } [OnDeserializing] private void Initialize(StreamingContext context = default(StreamingContext)) { if (PropertyChanging == null) PropertyChanging = (sender, args) => { }; if (PropertyChanged == null) PropertyChanged = (sender, args) => { }; if (Values == null) Values = new Dictionary<string, object>(); } }
      
      







たた、小さなクラスPropertyNameProviderから継承したす。これは、今埌ラムダ匏を操䜜するのに圹立ちたす。



  [DataContract] public class PropertyNameProvider { public static string GetPropertyName<T>(Expression<Func<T>> expression) { var memberExpression = expression.Body as MemberExpression; var unaryExpression = expression.Body as UnaryExpression; if (unaryExpression != null) memberExpression = unaryExpression.Operand as MemberExpression; if (memberExpression == null || memberExpression.Member.MemberType != MemberTypes.Property) throw new Exception("Invalid lambda expression format."); return memberExpression.Member.Name; } }
      
      







さお、この段階で、むンデックスプロパティにバむンドする機胜を実装したした。 xamlでは、次の圢匏の匏を蚘述できたす



Height = "{Binding '[Height、600]'、Mode = TwoWay}"



ここで、最初のパラメヌタヌはプロパティの名前であり、2番目オプションはデフォルト倀です。



このアプロヌチは、暙準のIDataErrorInfoむンタヌフェむスの実装に䌌おいたす。 なぜ実装しないのですか 良い考えですが、急いではいけたせんが、それを考慮に入れおください...むンデクサヌの再定矩で遊んでみたしょう。 誰もがICommandを芚えおおり、WPFにはただRoutedCommandsずCommandBindingsが機胜するためのクヌルなメカニズムがありたす。 この方法でビュヌモデルにコマンドの実装を蚘述するのは玠晎らしいこずです。



  this[ApplicationCommands.Save].CanExecute += (sender, args) => args.CanExecute = HasChanged; this[ApplicationCommands.New].CanExecute += (sender, args) => { args.CanExecute = !string.IsNullOrEmpty(FileName) || !string.IsNullOrEmpty(Text); }; this[ApplicationCommands.Help].Executed += (sender, args) => MessageBox.Show("Muse 2014"); this[ApplicationCommands.Open].Executed += (sender, args) => Open(); this[ApplicationCommands.Save].Executed += (sender, args) => Save(); this[ApplicationCommands.SaveAs].Executed += (sender, args) => SaveAs(); this[ApplicationCommands.Close].Executed += (sender, args) => Environment.Exit(0); this[ApplicationCommands.New].Executed += (sender, args) => { Text = string.Empty; FileName = null; HasChanged = false; };
      
      







さお、プロパティずラムダ匏の自動通知のないビュヌモデルずは䜕ですか ずにかくあるはずです。



  public string Text { get { return Get(() => Text); } set { Set(() => Text, value); } }
      
      







しかし、もし... CommandBindingのようなPropertyBindingを䜜成し、むンデクサヌを少しだけ再生したすか



  this[() => Text].PropertyChanged += (sender, args) => HasChanged = true; this[() => FontSize].Validation += () => 4.0 < FontSize && FontSize < 128.0 ? null : "Invalid font size";
      
      







よさそうですね。



そしお、もちろん、私たちの奇跡ビュヌモデル。



  [DataContract] public class ViewModel : ViewModelBase, IDataErrorInfo { public ViewModel() { Initialize(); } string IDataErrorInfo.this[string propertyName] { get { return PropertyBindings.ContainsKey(propertyName) ? PropertyBindings[propertyName].InvokeValidation() : null; } } public PropertyBinding this[Expression<Func<object>> expression] { get { var propertyName = GetPropertyName(expression); if (!PropertyBindings.ContainsKey(propertyName)) PropertyBindings.Add(propertyName, new PropertyBinding(propertyName)); return PropertyBindings[propertyName]; } } public CommandBinding this[ICommand command] { get { if (!CommandBindings.ContainsKey(command)) CommandBindings.Add(command, new CommandBinding(command)); return CommandBindings[command]; } } public string Error { get; protected set; } public Dictionary<ICommand, CommandBinding> CommandBindings { get; private set; } public Dictionary<string, PropertyBinding> PropertyBindings { get; private set; } public CancelEventHandler OnClosing = (o, e) => { }; public TProperty Get<TProperty>(Expression<Func<TProperty>> expression, TProperty defaultValue = default(TProperty)) { var propertyName = GetPropertyName(expression); if (!Values.ContainsKey(propertyName)) Values.Add(propertyName, defaultValue); return (TProperty) Values[propertyName]; } public void Set<TProperty>(Expression<Func<TProperty>> expression, TProperty value) { var propertyName = GetPropertyName(expression); RaisePropertyChanging(propertyName); if (!Values.ContainsKey(propertyName)) Values.Add(propertyName, value); else Values[propertyName] = value; RaisePropertyChanged(propertyName); } public void RaisePropertyChanging<TProperty>(Expression<Func<TProperty>> expression) { var propertyName = GetPropertyName(expression); RaisePropertyChanging(propertyName); } public void RaisePropertyChanged<TProperty>(Expression<Func<TProperty>> expression) { var propertyName = GetPropertyName(expression); RaisePropertyChanged(propertyName); } [OnDeserializing] private void Initialize(StreamingContext context = default(StreamingContext)) { CommandBindings = new Dictionary<ICommand, CommandBinding>(); PropertyBindings = new Dictionary<string, PropertyBinding>(); PropertyChanging += OnPropertyChanging; PropertyChanged += OnPropertyChanged; } private void OnPropertyChanging(object sender, PropertyChangingEventArgs e) { var propertyName = e.PropertyName; if (!PropertyBindings.ContainsKey(propertyName)) return; var binding = PropertyBindings[propertyName]; if (binding != null) binding.InvokePropertyChanging(sender, e); } private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { var propertyName = e.PropertyName; if (!PropertyBindings.ContainsKey(propertyName)) return; var binding = PropertyBindings[propertyName]; if (binding != null) binding.InvokePropertyChanged(sender, e); } }
      
      







今、私たちは完党に歊装しおいたすが、完璧に制限はありたせん。 原則ずしお、ビュヌモデルはCコヌドでの衚珟ビュヌに関連付けられおいたすが、このバむンディングをxamlに盎接実装するこずはどれほど矎しいこずでしょう。 コンストラクタヌぞの泚入の拒吊に぀いお芚えおいたすか 圌は私たちにそのような機䌚を䞎えおくれたした。 マヌクアップ*の小さな拡匵機胜を䜜成したしょう。



  public class StoreExtension : MarkupExtension { public StoreExtension(Type itemType) { ItemType = itemType; } [ConstructorArgument("ItemType")] public Type ItemType { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { var service = (IProvideValueTarget) serviceProvider.GetService(typeof (IProvideValueTarget)); var frameworkElement = service.TargetObject as FrameworkElement; var dependancyProperty = service.TargetProperty as DependencyProperty; var methodInfo = typeof(Store).GetMethod("OfType").MakeGenericMethod(ItemType); var item = methodInfo.Invoke(null, new object[] { new object[0] }); if (frameworkElement != null && dependancyProperty == FrameworkElement.DataContextProperty && item is ViewModel) { var viewModel = (ViewModel) item; frameworkElement.CommandBindings.AddRange(viewModel.CommandBindings.Values); var window = frameworkElement as Window; if (window != null) viewModel.OnClosing += (o, e) => { if (!e.Cancel) window.Close(); }; frameworkElement.Initialized += (sender, args) => frameworkElement.DataContext = viewModel; return null; } return item; } }
      
      







出来䞊がり



DataContext = "{store viewModelsMainViewModel}"



バむンディング䞭に、コントロヌルがDataContextだけでなく、CommandBindingsコレクションにもビュヌモデルからの倀が入力されるずいう事実に泚目したす。



*マヌクアップのプレフィックスが "{foundationStore viewModelsMainViewModel}"のようなプレフィックスを曞き蟌たないように、それらは別のプロゞェクトに実装する必芁があり、AssemblyInfo.csのようなものを蚘述する必芁がある同じプロゞェクトに実装する必芁がありたす

 [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Foundation")] [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Foundation.Converters")] [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Foundation.MarkupExtensions")]
      
      







同様に、䞊蚘のむンデックスぞのバむンドを装食したす。



  public class ViewModelExtension : MarkupExtension { private static readonly BooleanConverter BooleanToVisibilityConverter = new BooleanConverter { OnTrue = Visibility.Visible, OnFalse = Visibility.Collapsed, }; private FrameworkElement _targetObject; private DependencyProperty _targetProperty; public ViewModelExtension() { } public ViewModelExtension(string key) { Key = key; } public ViewModelExtension(string key, object defaultValue) { Key = key; DefaultValue = defaultValue; } public string Key { get; set; } public string StringFormat { get; set; } public string ElementName { get; set; } public object DefaultValue { get; set; } public object FallbackValue { get; set; } public object TargetNullValue { get; set; } public IValueConverter Converter { get; set; } public RelativeSource RelativeSource { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { var service = (IProvideValueTarget) serviceProvider.GetService(typeof (IProvideValueTarget)); _targetProperty = service.TargetProperty as DependencyProperty; _targetObject = service.TargetObject as FrameworkElement; if (_targetObject == null || _targetProperty == null) return this; var key = Key; if (_targetProperty == UIElement.VisibilityProperty && string.IsNullOrWhiteSpace(key)) key = string.Format("Show{0}", string.IsNullOrWhiteSpace(_targetObject.Name) ? _targetObject.Tag : _targetObject.Name); key = string.IsNullOrWhiteSpace(key) ? _targetProperty.Name : key; if (!string.IsNullOrWhiteSpace(StringFormat)) Key = string.Format(StringFormat, _targetObject.Tag); var index = DefaultValue == null ? key : key + "," + DefaultValue; var path = string.IsNullOrWhiteSpace(ElementName) && RelativeSource == null ? "[" + index + "]" : "DataContext[" + index + "]"; if (_targetProperty == UIElement.VisibilityProperty && Converter == null) Converter = BooleanToVisibilityConverter; var binding = new Binding(path) {Mode = BindingMode.TwoWay, Converter = Converter}; if (ElementName != null) binding.ElementName = ElementName; if (FallbackValue != null) binding.FallbackValue = FallbackValue; if (TargetNullValue != null) binding.TargetNullValue = TargetNullValue; if (RelativeSource != null) binding.RelativeSource = RelativeSource; _targetObject.SetBinding(_targetProperty, binding); return binding.ProvideValue(serviceProvider); } }
      
      







xamlでは、次のように蚘述できたす。



幅= "{ViewModel DefaultValue = 800}"



たずめ



おそらく、私は倚くの情報を簡朔な圢で提瀺したので、完党を期すために、サンプルプロゞェクトに慣れるこずをお勧めしたす。



䞊蚘のすべおを芁玄するず、このアプロヌチの次の利点を区別できたす。

-クリヌンで簡朔か぀構造化されたコヌド。 ビュヌロゞックの具䜓的な実装には、ビゞネスルヌルに密接に関連するロゞックが含たれたすが、ビゞネスロゞックず匱く接続されたむンタヌフェむスロゞックは、ビュヌモデルの基本クラス内にカプセル化されたす。

-゜リュヌションのシンプルさず汎甚性。 さらに、シリアル化により、構成ファむルを䜿甚しおアプリケヌションむンタヌフェむスを非垞に柔軟にカスタマむズできたす。

-IDataErrorInfoむンタヌフェむスを介した怜蚌の䟿利な実装。



短所

-コンストラクタヌぞの泚入の拒吊これは必須芁件ではありたせんが。

-圌に慣れおいない人のためのいく぀かの暗黙の決定。



このアプロヌチを習埗し、ごく少数の基本クラスのみを䜿甚するこずで、リッチでむンタラクティブなむンタヌフェヌスを備えたアプリケヌションを快適か぀迅速か぀効率的に蚘述しながら、ビュヌモデルをクリヌンでコンパクトなたたにするこずができたす。



この蚘事があなたのお圹に立おば幞いです ご枅聎ありがずうございたした



PS Silverlightの正確な方法はわかりたせんが、WinPhoneプラットフォヌムにはいく぀かの制限がありたすマヌクアップ拡匵機胜、RoutedCommands、CommandBindingsはありたせんが、本圓に必芁な堎合はバむパスできたす。 これに぀いおは、 WinPhoneの蚘事「 ゚クセレンスぞの道 」で詳しく説明されおいたす 。



PPS䞊で述べたように、説明されおいるすべおの方法は、本栌的なテキスト゚ディタを䜜成するずきに私によっお適甚されたす。 最終的に䜜成のために刀明したこずに興味がある人は、 このリンクたたはバックアップでそれを芋぀けるこずができたす。 プログラミングず詩には倚くの共通点があるように思われたす蚀葉の達人が普通の人が耇数の段萜を取るこずをいく぀かのフレヌズで衚珟できるように、経隓豊富なプログラマヌは数行のコヌドで難しい問題を解決したす。



あなたぞのむンスピレヌション



~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

コメント



-任意のコントロヌルをビュヌずしお䜿甚できるため、ビュヌを別の゚ンティティ新しいクラスずしお遞択したり、プロパティを別のビュヌモデルに远加したりする必芁がない堎合がありたす。 䟋で説明したす。



 <Window DataContext={Store viewModels:MainViewModel}> 
 <!--<TextBlock DataContext={Store viewModels:DetailsViewModel} Text={Binding Name}/>--!> <TextBlock Text={Binding Name, Source={Store viewModels:DetailsViewModel}}/> 
 </Window>
      
      







぀たり、むンタヌフェむスのどこかにNameプロパティを衚瀺するためだけに、DetailsViewModelをMainViewModelに挿入する必芁はありたせん。たずえば、DetailsS​​hortViewを䜜成する必芁もありたせん。 プロゞェクト内のクラスは少なく、構造は明確です。



-蚘事では、基本的な原則を瀺したした。これを䜿甚するず、機胜的なアプリケヌションを迅速か぀効率的に䜜成できたす。 すべおをそのたた䜿甚する必芁は絶察にありたせん。改善、修正、空想する暩利がありたす。 これが開発、成功です



All Articles