質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.51%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

MVVM

MVVM(Model View ViewModel)は構築上のデザインパターンで、表現ロジック(ViewModel)によってデータ(Model)からページ(View)を分離させます。

Q&A

解決済

3回答

2835閲覧

INotifyPropertyChangedの実装をなるべく簡略化したい

sh_akira

総合スコア380

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

MVVM

MVVM(Model View ViewModel)は構築上のデザインパターンで、表現ロジック(ViewModel)によってデータ(Model)からページ(View)を分離させます。

5グッド

4クリップ

投稿2018/03/22 07:22

編集2018/03/23 05:04

前提・実現したいこと

INotifyPropertyChangedの実装をなるべく短く書きたい(速度は度外視で)ので
次のようなViewModelBaseクラスを作成しました。

csharp

1public abstract class ViewModelBase : INotifyPropertyChanged 2{ 3 4 public event PropertyChangedEventHandler PropertyChanged; 5 6 protected virtual void RaisePropertyChanged(string propertyName) 7 { 8 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 9 } 10 11 protected virtual void RaisePropertyChanged(params string[] propertyNames) 12 { 13 foreach (var propertyName in propertyNames) RaisePropertyChanged(propertyName); 14 } 15 16 private Dictionary<string, object> PropertyDictionary = new Dictionary<string, object>(); 17 18 protected virtual void Setter(object value, [System.Runtime.CompilerServices.CallerMemberName] string PropertyName = "") 19 { 20 if (PropertyDictionary.ContainsKey(PropertyName)) 21 { 22 if (PropertyDictionary[PropertyName] == value) return; 23 PropertyDictionary[PropertyName] = value; 24 } 25 else 26 { 27 PropertyDictionary.Add(PropertyName, value); 28 } 29 RaisePropertyChanged(PropertyName); 30 } 31 32 protected virtual object Getter(string PropertyName) 33 { 34 return PropertyDictionary.TryGet(PropertyName); 35 } 36 37 protected virtual T Getter<T>([System.Runtime.CompilerServices.CallerMemberName] string PropertyName = "") 38 { 39 var ret = Getter(PropertyName); 40 if (ret == null) return default(T); 41 return (T)ret; 42 } 43} 44 45文字数制限で省略(編集履歴参照)

プロパティ名をCallerMemberNameでもらって、Dictionary<string, object>にすべての値を入れる方式です。
取得と設定にはGetterとSetterを使用して、次のような書き方が出来ます。

csharp

1public class Sample : ViewModelBase 2{ 3 public bool IsExpanded { get => Getter<bool>(); set => Setter(value); } 4 public bool? IsChecked { get => Getter<bool?>(); set => Setter(value); } 5 public string Text { get => Getter<string>(); set => Setter(value); } 6 public object Data { get => Getter<object>(); set => Setter(value); } 7 public Sample Parent { get => Getter<Sample>(); set => Setter(value); } 8 public ObservableCollection<Sample> Children { get => Getter<ObservableCollection<Sample>>(); set => Setter(value); } 9}

###質問
基本的に使用する際は

csharp

1public 型 名前 { get => Getter<>(); set => Setter(value); }

これだけ書けばよいので短くて気に入っています。
ただ、同じ型名を2度書かないといけないため、ObservableCollectionだったりすると
かなり長くなってしまうのが気に入らないので、何か別の方法で、さらに短くできないかという質問です。
具体的には2度書いている型を1度にできないのか?returnでの型推論は効かせる方法はないのか?です。

###追記:仮想プロパティをオーバーライドするクラス動的生成

Zuishinさんの提案を参考に下記のように書き換えました。

csharp

1public abstract class ViewModelBase : INotifyPropertyChanged 2{ 3 4 public event PropertyChangedEventHandler PropertyChanged; 5 6 protected virtual void RaisePropertyChanged(string propertyName) 7 { 8 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 9 } 10 11 //ref:https://qiita.com/Zuishin/items/0b7080e6f7e277d9394b 12 //ref:http://www.gutgames.com/post/Overridding-a-Property-With-ReflectionEmit.aspx 13 private static Dictionary<Type, Type> typeDictionary = new Dictionary<Type, Type>(); 14 public static T Create<T>(params object[] parameters) where T : ViewModelBase 15 { 16 if (!typeDictionary.TryGetValue(typeof(T), out Type type)) 17 { 18 var name = "ViewModel_" + Guid.NewGuid().ToString("N"); 19 var assemblyName = new AssemblyName(name); 20 var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( 21 assemblyName, 22 AssemblyBuilderAccess.RunAndCollect); 23 var moduleBuilder = assemblyBuilder.DefineDynamicModule(name); 24 var typeBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class, typeof(T)); 25 //プロパティはvirtualではなくアクセサがvirtualになる。 26 var virtualProperties = typeof(T).GetProperties().Where(p => p.GetAccessors().Any(a => a.IsVirtual)); 27 foreach (var property in virtualProperties) 28 { 29 /* 30 * プロパティのオーバーライドするときは、Getter/Setterだけオーバーライドしないといけないらしい 31 * DefinePropertyでプロパティまで作ってしまうと、新クラスとベースクラスに二つのプロパティが生まれて 32 * 正しく動作しなくなってしまった 33 */ 34 35 //新しいプロパティ 36 //var propertyBuilder = typeBuilder.DefineProperty( 37 // property.Name, 38 // PropertyAttributes.None, 39 // property.PropertyType, 40 // new Type[] { property.PropertyType } 41 // ); 42 //フィールド 43 //バッキングフィールドは直接触れなかった 44 //var field = typeof(T).GetField($"<{property.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); 45 //なので新しいフィールド作成 46 var field = typeBuilder.DefineField($"field_{property.PropertyType}", property.PropertyType, FieldAttributes.Private); 47 48 var accessorAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual; 49 50 //Getter 51 var getMethod = typeBuilder.DefineMethod($"get_{property.Name}", accessorAttr, property.PropertyType, Type.EmptyTypes); 52 var getIL = getMethod.GetILGenerator(); 53 getIL.Emit(OpCodes.Ldarg_0); 54 getIL.Emit(OpCodes.Ldfld, field); 55 getIL.Emit(OpCodes.Ret); 56 57 //propertyBuilder.SetGetMethod(getMethod); 58 59 //Setter 60 var setMethod = typeBuilder.DefineMethod($"set_{property.Name}", accessorAttr, null, new Type[] { property.PropertyType }); 61 var setIL = setMethod.GetILGenerator(); 62 setIL.Emit(OpCodes.Ldarg_0); 63 setIL.Emit(OpCodes.Ldarg_1); 64 setIL.Emit(OpCodes.Stfld, field); 65 setIL.Emit(OpCodes.Ldarg_0); 66 setIL.Emit(OpCodes.Ldstr, property.Name); 67 var meth = typeof(T).GetMethod("RaisePropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, Type.DefaultBinder, new Type[] { typeof(string) }, null); 68 setIL.Emit(OpCodes.Callvirt, meth); 69 setIL.Emit(OpCodes.Ret); 70 71 //propertyBuilder.SetSetMethod(setMethod); 72 } 73 type = typeBuilder.CreateType(); 74 typeDictionary[typeof(T)] = type; 75 } 76 return (T)Activator.CreateInstance(type, parameters); 77 } 78}

csharp

1public class CheckTreeSource : ViewModelBase 2{ 3 public virtual bool IsExpanded { get; set; } 4 public virtual bool? IsChecked { get; set; } 5 public virtual string Text { get; set; } 6 public virtual object Data { get; set; } 7 public virtual CheckTreeSource Parent { get; set; } 8 public virtual ObservableCollection<CheckTreeSource> Children { get; set; } 9}

使用方法は

csharp

1var item = ViewModelBase.Create<CheckTreeSource>();

動作確認に使用したコードは

XAML

1<Window x:Class="ViewModelSample.MainWindow" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 5 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 6 xmlns:local="clr-namespace:ViewModelSample" 7 mc:Ignorable="d" 8 Title="MainWindow" Height="450" Width="800"> 9 <DockPanel> 10 <Button DockPanel.Dock="Top" Content="CheckAllItems" Click="Button_Click"/> 11 <TreeView Name="CheckTreeView"> 12 <TreeView.Resources> 13 <Style TargetType="TreeViewItem"> 14 <Setter Property="IsExpanded" Value="{Binding Path=IsExpanded,Mode=TwoWay}"/> 15 </Style> 16 </TreeView.Resources> 17 <TreeView.ItemTemplate> 18 <HierarchicalDataTemplate DataType="local:CheckTreeSource" ItemsSource="{Binding Children}"> 19 <CheckBox Margin="1" IsChecked="{Binding IsChecked}"> 20 <TextBlock Text="{Binding Text}"/> 21 </CheckBox> 22 </HierarchicalDataTemplate> 23 </TreeView.ItemTemplate> 24 </TreeView> 25 </DockPanel> 26</Window> 27

csharp

1public partial class MainWindow : Window 2{ 3 private ObservableCollection<CheckTreeSource> checkItems = new ObservableCollection<CheckTreeSource>(); 4 public MainWindow() 5 { 6 InitializeComponent(); 7 var random = new Random(); 8 9 10 foreach (var i in Enumerable.Range(0, 20)) 11 { 12 var item = ViewModelBase.Create<CheckTreeSource>(); 13 item.Text = $"Parent{i}"; 14 item.IsChecked = random.Next(2) == 0; 15 item.IsExpanded = true; 16 checkItems.Add(item); 17 } 18 19 foreach (var item in checkItems) 20 { 21 item.Children = new ObservableCollection<CheckTreeSource>(); 22 foreach (var i in Enumerable.Range(0, 3)) 23 { 24 var childitem = ViewModelBase.Create<CheckTreeSource>(); 25 childitem.Parent = item; 26 childitem.Text = $"Child{i}"; 27 childitem.IsChecked = item.IsChecked; 28 item.Children.Add(childitem); 29 } 30 } 31 32 CheckTreeView.ItemsSource = checkItems; 33 } 34 35 private void Button_Click(object sender, RoutedEventArgs e) 36 { 37 foreach(var item in checkItems) 38 { 39 item.IsChecked = true; 40 foreach(var childitem in item.Children) 41 { 42 childitem.IsChecked = true; 43 } 44 } 45 } 46}

初めてILに触ったのでとても勉強になりました。
プロパティのオーバーライドもgetter/setterだけオーバーライドでいいと思わず、新発見でした。
値を入れてたDictionaryも無くなってとってもいい感じです。

kleus_balut, fijino, hihijiji, Zuishin, umyu👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

ベストアンサー

まだありません。
しかし実装される可能性はあります。

[雑記] 型推論の是非

対案: 左辺から右辺の型推論

この通り、C#では、ローカル変数以外のvarによる型推論は、おそらくずっと認められることはないでしょう。 代わりと言ってはなんですが、「逆向きの型推論」が入る可能性はあります。 すなわち、以下のような書き方です。

中略

この構文は、早ければC# 8あたり(2017~2018年頃?)で入りそうです。

投稿2018/03/22 08:51

Zuishin

総合スコア28656

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sh_akira

2018/03/22 09:23

ありがとうございます。 returnの型は決まっているので、その型でジェネリックメソッドを呼び出すというのは、 逆向きの型推論になるというわけですね。 余り多用したくない構文な気がしますが、今回の場合やリンク先の例の時のような分かりやすい場合は 使えると嬉しそうです。
Zuishin

2018/03/22 15:07

https://qiita.com/Zuishin/items/0b7080e6f7e277d9394b で「C# で無駄に難しくメモ化をしてみた」という記事を書きましたが、これは結局どうやっているかというとユーザーが作ったクラス(この質問の場合だと ViewModelBase)を継承した新しいクラスを動的に作成してメソッドをオーバーライドしています。 サブクラスはポリモーフィズムでスーパークラスと同じように働きますからインスタンスを作成してしまえば継承したことなど忘れて使うことができます。 同様に仮想プロパティを継承して INotifyPropertyChanged を自動的に実装するような仕組みを作れば最も簡単にできるかもしれませんね。 IL を組むのが面倒なので今は作りませんが後日暇だったらやるかもしれません。
sh_akira

2018/03/23 01:20

その記事は以前読ませていただいてました。 確かに仮想プロパティを探して、INotifyPropertyChangedを自動で実装できれば短くできますね。 ちょっとやってみます。
sh_akira

2018/03/23 05:07

やってみました。質問に追記しています。初めてIL触ってみましたが、動いているようです。 ViewModelBaseを継承して、プロパティにvirtualを付ければ、 ViewModelBase.Create<Sample>();とするだけで、自動でINotifyPropertyChangedのイベントが 各プロパティに実装されてオーバーライドできるようになりました。 プロパティ自体はオーバーライドせずにアクセサだけで良かったりと色々と発見があり、楽しかったです。 質問文の文字数制限でお礼まで入りませんでした。ありがとうございました。
Zuishin

2018/03/23 05:10

おお、早かったですね。素晴らしい!
guest

0

INotifyPropertyChangedを書きたくないなら、ReactivePropertyを使えばいいじゃない。
https://qiita.com/YSRKEN/items/5a36fb8071104a989fb8

投稿2018/03/22 17:25

kiichi54321

総合スコア1984

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sh_akira

2018/03/23 01:17

普通に使うときはReactivePropertyでいいと思います。というより私も使ってます。 今回はどちらかというとこれ以上短く書けないのかという技術的興味です。
guest

0

具体的には2度書いている型を1度にできないのか?

できますが、型名の代わりに型の値が引数に必要です。
例えば以下のようなメソッドをViewModelBaseに追加します。

csharp

1/// <summary> 2/// 現在のプロパティ値を取得 3/// </summary> 4/// <param name="initialValue">初期値</param> 5protected T Getter<T>(T initialValue, [CallerMemberName]string propertyName = null) 6{ 7 //キーに値が無かったら初期値を現在値に入力 8 if (!PropertyDictionary.ContainsKey(propertyName)) 9 PropertyDictionary[propertyName] = initialValue; 10 11 //Dictionaryから現在値を取得してプロパティの型に変換する 12 return Getter<T>(propertyName); 13}

使用する際はSampleクラスで

public bool IsExpanded { get => Getter(false); set => Setter(value); }

といった形になります。
注意点として、Getterよりも先にSetterを呼ぶと初期値が使用されなくなります。

投稿2018/03/22 14:24

soi013

総合スコア149

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Zuishin

2018/03/22 14:29

私もそのような実装をしたことがありますが、初期値が null の場合が多くて使い物になりませんでした。 Setter を先に読んだ場合に初期値が使用されなくなるのはいいのですが、私の場合は IEditable も実装していたために初期値が BeginEdit() でバックアップされなくてはまりました。
soi013

2018/03/22 15:04

> BeginEdit() でバックアップされなくて IEditableは使用したことがないですが、初期化のタイミングによって問題が発生しそうですね。 > 初期値が null の場合が多くて 組み込み型以外では使いにくいかもしれませんね
sh_akira

2018/03/23 01:15

確かに値から推論させれば型は1度で済みますが、nullを入れたい場面が多いですね。 new ObservableCollection<Sample>()とかだと余計長くなっちゃいます。 これは使うときに結局書くのでソース全体の文字数では少なくなりますが。。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.51%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問