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

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

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

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

MVVM

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

WPF

Windows Presentation Foundation (WPF) は、魅力的な外観のユーザー エクスペリエンスを持つ Windows クライアント アプリケーションを作成するための次世代プレゼンテーション システムです

Q&A

解決済

1回答

2299閲覧

C# WPF MVVM 階層的なモデルのプロパティの変更を受けて、別のモデルのプロパティの値を変更したい

退会済みユーザー

退会済みユーザー

総合スコア0

C#

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

MVVM

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

WPF

Windows Presentation Foundation (WPF) は、魅力的な外観のユーザー エクスペリエンスを持つ Windows クライアント アプリケーションを作成するための次世代プレゼンテーション システムです

0グッド

1クリップ

投稿2021/10/15 10:20

編集2021/10/16 06:35

前提・実現したいこと

「該当のソースコード」に記述したような階層的なクラスがあって、PropertyInt1項目を変更すると、何かしら計算してPropertyInt2項目に計算結果を反映したいです。

該当のソースコード

手元にビルドの環境がなく、雰囲気だけのコードですが記述してみました。
(実際の今のコードは自動実装プロパティが定義されているだけで、INotifyPropertyChangedなどは実装されていません。同期をとる処理を試すために修正したものが以下です。)

cs

1public class Class0 2{ 3 public Class1 PropertyClass1 { get; set; } = new Class1(); 4 public Class2 PropertyClass2 { get; set; } = new Class2(); 5 public Class0() 6 { 7 PropertyChangedEventManager.AddHandler(PropertyClass1.PropertyClass11, OnPropertyChanged, nameof(Class11.PropertyInt1)); 8 } 9 ~Class0() 10 { 11 PropertyChangedEventManager.RemoveHandler(PropertyClass1.PropertyClass11, OnPropertyChanged, nameof(Class11.PropertyInt1)); 12 } 13 public void OnPropertyChanged(object sender, PropertyChangedEventArgs e) 14 { 15 if (!(sender is Class11)) return; 16 if (e.PropertyName == nameof(Class11.PropertyInt1)) 17 { 18 PropertyClass2.PropertyClass21.PropertyInt2 = PropertyClass1.PropertyClass11.PropertyInt1 * 2; 19 } 20 } 21} 22public class Class1 23{ 24 public Class11 PropertyClass11 { get; set; } = new Class11(); 25} 26public class Class11 : INotifyPropertyChanged 27{ 28 public event PropertyChangedEventHandler PropertyChanged; 29 private void NotifyPropertyChanged([CallerMemberName] String propertyName = "") 30 { 31 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 32 } 33 private int _propertyInt1; 34 public int PropertyInt1 35 { 36 get => _propertyInt1; 37 set 38 { 39 if (_propertyInt1 == value) return; 40 _propertyInt1 = value; 41 NotifyPropertyChanged(); 42 } 43 } 44} 45public class Class2 46{ 47 public Class21 PropertyClass21 { get; set; } = new Class21(); 48} 49public class Class21 50{ 51 public int PropertyInt2 { get; set; } 52} 53 54public class Test 55{ 56 public void TestMethod() 57 { 58 var class0 = new Class0(); 59 class0.PropertyClass1.PropertyClass11.PropertyInt1 = 10; 60 // class0.PropertyClass2.PropertyClass21.PropertyInt2 == 20; 61 62 class0.PropertyClass1.PropertyClass11 = new Class11(); 63 class0.PropertyClass1.PropertyClass11.PropertyInt1 = 30; 64 // class0.PropertyClass2.PropertyClass21.PropertyInt2 == 60; // ← イベントが発生しないため60にはならない 65 } 66}

試したこと

プロパティの変更通知のイベントを購読して実装をしてみましたが、問題があるようでした。
PropertyClass1、PropertyClass11のプロパティ自体が変更された場合に、コンストラクタで購読設定したインスタンスは、現在保持しているインスタンスのものではなくなってしまい、PropertyInt1項目を変更してもイベントが発生しないことです。
※シリアル化を使ってClass0のインスタンスを生成しているところがあるのですが、シリアル化の動きとして、プロパティの初期値として指定してあるインスタンス(new Class1()の部分)は、シリアル化で生成されたインスタンスで上書きされてしまうようでした。
そのため、Class0のコンストラクタで購読したインスタンスは、シリアル化から上書きされることによって、参照されない場所に消え去ってしまっていたようでした。

そこで、PropertyClass1、PropertyClass11が変更された場合にイベントを購読し直せば良いのではないかと考えました。
しかし、購読する項目が少ないうちは大丈夫そうですが、項目が増えた場合にメンテナンスしていけなそうに感じています。
次の記事の「リアクティブスパゲティ」という状況になりそうなイメージでしょうか。

初期化以外でSubscribeを書かない

リアクティブスパゲティを避けるための2つの原則 │ Aiming 開発者ブログ

MVVMの記事などを見るのですが、ビューとビューモデルについてだけしか記載されていないことが多く、モデルをどう実装するのかよく理解できていないところがあるのかもしれません。
次の記事の「Model間での変更通知」のセオリーのようなものが知りたいです。
※以下の記事では個人で開発されたStatefulModelというパッケージを使用することの説明がされていると思いますが、それ以外の開発者はこのパッケージを使用せずにMVVMを実現されていると思いますので、その場合のセオリーのようなものが知りたいです

イベント購読の問題

M-V-WhateverのWhateverではModelの変更通知をどう監視しますか?また、Model間での変更通知は?

StatefulModelについて - the sea of fertility

最後に、PropertyInt1が設定されるのは画面から入力される時で、画面の項目にClass0クラスから辿ったパスがバインドされています。
ですので、「該当のソースコード」のTestクラスに記述したイメージで、PropertyInt1項目を設定しているところで、PropertyInt2を設定する処理も記述するということはできなそうだと思っています。

補足情報(FW/ツールのバージョンなど)

  • Windows 10
  • C# 7
  • .NET Framework 4.5
  • Visual Studio 2019
  • WPF

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

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

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

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

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

TN8001

2021/10/15 14:49

> PropertyInt1項目を変更すると、何かしら計算してPropertyInt2項目に計算結果を反映したいです。 なんで密接に関係しそうな2つのプロパティがそんなに離れているんでしょうか? 常に2倍(コード上はそうなっていないので常にってわけではない?)になるんだったら、そもそもシリアライズする必要もないのでは?(ViewModelで作るとかコンバータで変換するとか) > シリアル化で生成されたインスタンスで上書きされてしまうようでした。 シリアライザが不明ですが何を使っていますか?(OnDeserializedがあればそこで購読は可能でしょうけど) > 次の記事の「Model間での変更通知」のセオリーのようなものが知りたいです。 シリアライズの話しか出ていないのでなんともいえませんが、提示のクラスはValueObjectあるいはINotifyPropertyChangedのみにして、統括するようなModelがあってよさそうな気がするのですが。 もうちょっと困っている具体例がほしいですね。 ふわっとした質問にはふわっとした回答しか付かないかもしれません。 --- > 手元にビルドの環境がなく、雰囲気だけのコードですが記述してみました。 雰囲気で書いて間違いが1個(Testにメソッドがない)はすごいですね。 わたしはVSなしで書ける気しませんw
退会済みユーザー

退会済みユーザー

2021/10/16 02:24

書き込みありがとうございます。 当たり前が何か分からず悩んでしまったので、ご意見いただけて嬉しいです。 > なんで密接に関係しそうな2つのプロパティがそんなに離れているんでしょうか? 2階層目のプロパティ(Class1, Class2)は業務的なルールがあって、プリミティブな値を持つ項目はどちらかに分類されることになっています。 (実際はClass3とかClass4のようにもう少し分類があります) 3階層目以下のプロパティ(Class11, Class21)は人間が把握しやすいようにグループ化する目的で作られている程度です。 ”業務的なルール”が、業界の中でも専門的な位置にあり、私自身よく理解できていません。 業界の中の人からしたら当たり前に分類されることが、プログラム的には扱いづらい形になってしまっているのかもしれません。 ただ、すでに稼働してしまっていて、今からこの構成を変えることは難しそうです。 ※プロジェクトの特性として、科学技術計算をしているのですが、現実の世界をそのまま表現しようとすると膨大な項目を扱わなくてはならないため、簡略化した形でモデリングしているそうです。  それでも、その業界に対する知識が素人の私が見ると、同じような項目がいくつも並んでいるように見えます。 > 常に2倍(コード上はそうなっていないので常にってわけではない?) 変更通知のイベントを購読したら常に2倍になる(同期がとれる)と思ったのですが、なっていないですか? > そもそもシリアライズする必要もないのでは?(ViewModelで作るとかコンバータで変換するとか) すみません、混乱させてしまったかもしれません。 2倍にすることと、シリアル化は直接的には関係がありません。 2倍にするため(同期をとるため)に、変更通知のイベントを購読するように修正してみたところ、逆シリアル化のタイミングで購読したインスタンスが消え去ってしまったため、”試したこと”として記述しました。 > シリアライザが不明ですが何を使っていますか?(OnDeserializedがあればそこで購読は可能でしょうけど) OnDeserializedというものがあるのですね(教えていただきありがとうございます)。 XmlSerializerを使っていますが、これにはOnDeserializedはなさそうに見えましたね。 そういうものがあることを教えてもらったので、OnDeserializedがあるシリアライザを使う機会があれば活かしたいと思います。 > シリアライズの話しか出ていないのでなんともいえませんが、提示のクラスはValueObjectあるいはINotifyPropertyChangedのみにして、統括するようなModelがあってよさそうな気がするのですが。 自分の試した修正後のコードのイメージを「該当のソースコード」に記述してしまいましたが、実際は今のコードはほとんどメソッドを持たないValueObjectの状態です(INotifyPropertyChangedも実装していません)。 (中途半端に修正したコードを貼り付けてしまってすみません) ”統括するようなModelのイメージ”ですが、Class0がすでにそれに近いもののような気がしました。 PropertyInt1とPropertyInt2の同期をとるとしたら以下のコードのようなイメージでしょうか。 これでも問題は解消できてはいるのですが、モデルが冗長的なことをやり過ぎているような感じも受けました。 ビューモデルの中にPropertyInt1を定義して、それのゲットアクセサーではモデルのPropertyInt1を返し、それのセットアクセサーでモデルのPropertyInt1とPropertyInt2の設定処理を記述するのなら、あまり冗長さは気にならないですが、これはビューモデルですることではないようにも感じました。 (モデルやビューモデルに対して私が感じていることはおかしいですか?) ```cs public class Class0 { public Class1 PropertyClass1 { get; set; } = new Class1(); public Class2 PropertyClass2 { get; set; } = new Class2(); // Class0のPropertyInt1を画面の項目にバインドする? public int PropertyInt1 { get => PropertyClass1.PropertyClass11.PropertyInt1; set { PropertyClass1.PropertyClass11.PropertyInt1 = value; PropertyClass2.PropertyClass21.PropertyInt2 = value * 2; } } } ``` ”ValueObject”というものをよく理解しておらず、雰囲気で値を持つだけのオブジェクトかな?と受け取りましたが、後から少し調べたところ、次の記事がありました。 今回のプロジェクトではこのモデルはイミュータブルではないため、完全に”ValueObject”というわけではないようでした(補足程度の情報までに、です)。 > イミュータブルオブジェクト > 不変性とも言います > オブジェクトが生成された時点でオブジェクトの値が変わることを禁止しています > [ValueObjectという考え方 - Qiita](https://qiita.com/kichion/items/151c6747f2f1a14305cc) > もうちょっと困っている具体例がほしいですね。 > ふわっとした質問にはふわっとした回答しか付かないかもしれません。 具体的でなくてすみません。 やりたいことはタイトルにある通りで「階層的なモデルのプロパティの変更を受けて、別のモデルのプロパティの値を変更したい」なんです。 単純な問題を難しく考えているだけのような気もしています。 実際”統括するようなModel”を作ればタイトルにある問題自体は解消できると思いました。 (でもこれはご意見をいただけて気づけたことでしたので質問してみてよかったと思っています) 根本にある問題は、上の方に記述した「(モデルやビューモデルに対して私が感じていることはおかしいですか?)」にあるような気がしています。 「”統括するようなModel”を作って、冗長的にプロパティを定義することがセオリーですよ」とお墨付きをいただけたらスッキリするのかもしれません。 > 雰囲気で書いて間違いが1個(Testにメソッドがない)はすごいですね。 ご指摘ありがとうございます。 後で修正しておきたいと思います。 メソッドの宣言をせずにクラスの直下に処理を記述してしまうとは、大胆な間違いをしてしまいましたw
TN8001

2021/10/16 04:51

> 変更通知のイベントを購読したら常に2倍になる(同期がとれる)と思ったのですが、なっていないですか? PropertyInt2の変更がPropertyClass11には反映されないので、双方向ではないという意味です。 > 実際は今のコードはほとんどメソッドを持たないValueObjectの状態です(INotifyPropertyChangedも実装していません)。 ただのプロパティなんですか。それをINotifyPropertyChanged(以下INPC)実装したのを試している。と 常に2倍が要件で別々の場所にあるのを動かせないなら、INPCにせざるを得ないかもしれませんねぇ。 > ”統括するようなModelのイメージ”ですが、Class0がすでにそれに近いもののような気がしました。 Class0がそれにあたるなら、自身をデシリアライズした後にイベント購読して返すようなファクトリメソッドとかはどうでしょうね? 購読解除がファイナライザはイヤ(タイミングが読めない)なので、IDisposableにしたいですね。 > これでも問題は解消できてはいるのですが、モデルが冗長的なことをやり過ぎているような感じも受けました。 ViewModelでModelをラップすることはままありますが、Class0でやるのはいまいちですよね(意味があって分けたのに全部ぶちまけているようなものですし^^; > (補足程度の情報までに、です)。 POCOとかのほうが適切だったでしょうか^^; > 「”統括するようなModel”を作って、冗長的にプロパティを定義することがセオリーですよ」とお墨付きをいただけたらスッキリするのかもしれません。 わたしはアマチュアなうえ小規模アプリしか作らないので、「業務でー」とか「一般的にー」とかはわかりません。 プロの方のご意見を伺いたいところですね。
退会済みユーザー

退会済みユーザー

2021/10/16 06:37

> PropertyInt2の変更がPropertyClass11には反映されないので、双方向ではないという意味です。 そう言われると、そうですね。 ちゃんと月曜日に確認しようと思いますが、このプロジェクトでは全部の項目を画面の項目に表示(入力)しているわけではなく、一方向で大丈夫なケースが多かった気がします。 それなら、項目は一方のものだけを定義して、もう一方は必要な時に計算するのでも良い(ゲットアクセサーのみとか)、気もしてきましたね(それが実際にやっても良いかどうかは上の許可が必要になりますが)。 ご意見ありがとうございます。 > ただのプロパティなんですか。それをINotifyPropertyChanged(以下INPC)実装したのを試している。と > 常に2倍が要件で別々の場所にあるのを動かせないなら、INPCにせざるを得ないかもしれませんねぇ。 そうです、現状、ただのプロパティが定義されただけのクラスで、INPCは同期を試した際のコードになっています(中途半端なコードですみません)。 やっぱりINPCになりますかねぇ。 > Class0がそれにあたるなら、自身をデシリアライズした後にイベント購読して返すようなファクトリメソッドとかはどうでしょうね? そういう方法も良いかもしれません。 ただ、INPCのイベントを購読する方針にするとして、今度はデシリアライズだけが問題ではなくなるような気がしています。 「該当のソースコード」のTestクラスを修正しました。 PropertyClass1やPropertyClass11が変更されると、イベントを購読している対象のインスタンスは現在保持しているPropertyClass11の中のインスタンスではないため、PropertyInt1を変更してもイベントが発生しないという問題です。 PropertyClass1やPropertyClass11が変更された際にも購読をし直すようにすれば良いのかとも思うのですが、色々なところでイベントを購読してしまうと、今度は処理を追うのが難しくなってしまうような気がしています。 > 購読解除がファイナライザはイヤ(タイミングが読めない)なので、IDisposableにしたいですね。 これもご意見ありがとうございます。 PropertyChangedEventManagerって使ったことがなくて、ファイナライザに書いてしまいました。 IDisposableにしようと思います。 > ViewModelでModelをラップすることはままありますが、Class0でやるのはいまいちですよね(意味があって分けたのに全部ぶちまけているようなものですし^^; 確かに全部ぶちまけている感ありますね(なかなか的確な表現で好きですw) > POCOとかのほうが適切だったでしょうか^^; 多分意味するものは伝わっていたと思いますので大丈夫です。 こちらの状況をできるだけ詳細に伝えたいと思ってのことでした。 > わたしはアマチュアなうえ小規模アプリしか作らないので、「業務でー」とか「一般的にー」とかはわかりません。 そうだったのですね。 ここまでご意見をいただけて、すごいと思いました。
TN8001

2021/10/16 08:41

> PropertyClass1やPropertyClass11が変更される 中間のクラスも入れ替わる可能性があるとなると、もう収拾つかなくなりますね^^; そうなるとこれらはDBやWebAPIの入出力用のクラスのようにPOCOの状態にして、より使いやすい構造のModel(実質的にはViewModel?)に入れなおすほうがましな気がします。 当然同じような定義を重複して書くことになってしまいますが。 いじれないならあきらめもつきますが(たまにひどい構造のJsonが返ってくるようなものがありますよね)、中途半端にいじれるとかえって困りますね^^;
退会済みユーザー

退会済みユーザー

2021/10/16 12:06

> 中間のクラスも入れ替わる可能性があるとなると、もう収拾つかなくなりますね^^; やっぱりそう感じるものなのですね。 ちゃんと調べてみないとはっきりしたことは言えないのですが、実際のところ、中間のクラスが入れ替わることは業務的にはなかったような気がしています。 ただ、コード的には入れ替えることは可能な形になってしまっていますので、今後も入れ替えるがないとは言い切れません。 (独り言です)使ったことはないのですが、Reactive Extensionsとか使うとこのような問題は起こらないものなのかなぁ。 > そうなるとこれらはDBやWebAPIの入出力用のクラスのようにPOCOの状態にして、より使いやすい構造のModel(実質的にはViewModel?)に入れなおすほうがましな気がします。 > 当然同じような定義を重複して書くことになってしまいますが。 そのようなイメージに落ち着くことになるのですかね。 調べていたら、次のような記事を見つけました。 > https://elf-mission.net/wp-content/uploads/2020/01/3-layered-architecture-mvvm.png > [DB が見えるのは嫌なので 3 階層 に AbstractFactory したいと思います。#4 | :: halation ghost ::](https://elf-mission.net/programming/wpf/mvvm-labo/phase04/) レイヤーが異なるのであれば、必要な項目を冗長的に定義すること程度は許容できそうかなと思っています。 一旦”より使いやすい構造のModel(実質的にはViewModel?)に入れなおす”方針でやってみたいと思います。 やってみないと分からないところもありそうですので、来週いっぱいを目処に結果を書き込みたいと思います。 色々ご意見をいただきありがとうございます。
guest

回答1

0

ベストアンサー

アドバイスいただいたように”より使いやすい構造のModel(実質的にはViewModel?)に入れなおす”方針で解決しました。
TN8001さんありがとうございました。

(自己解決として良いのかわからないですが)

投稿2021/10/21 09:36

退会済みユーザー

退会済みユーザー

総合スコア0

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

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

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

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

質問する

関連した質問