データクラスを入れ子にすること自体が推奨されないとのことですが、面倒くさいので入れ子にしてみる記事。
タイトルの通り、参照値渡し(参照型による値渡し)をします。
C#でデータクラスそのものをを参照値渡しすることにより、プロパティを介さずにデータクラス内の値の変化を反映させられることを前回書きました。
今のオブジェクト指向言語ならだいたいなんでもOKだと思います。
以下参考
WPFでボタン押下時の挙動をクラスに見立てています
これはList型についての記事
この参照型の値渡しにおいてデータクラスを入れ子にしたクラスにすれば、 上位のデータクラスを渡すだけでデータクラス全体の状態変化を通知できるのでは? と考えました※1。
$\tiny{ ※1.最初は継承すればよいと考えたけどかなり面倒なのでやめた。}$
Git
Gsudo
git clone https://github.com/Sheephuman/nested_data_classes_passing.git
データクラスA
Window状態のフラグ(bool値)を複数持っているデータクラス
※普通にデフォルトで取得できるけどわかりやすさ優先
using System.Windows.Media;
namespace 入れ子_ネスト_にしたデータクラスによる参照値渡し
{
public class DataClassA
{
public bool StateTopMost { get; set; }
public bool StateMaximamize { get; set; }
public bool StateEnabled { get; set; }
public bool StateVisiblity { get; set; }
public Brush? WindowBackColor { get; set;}
}
}
データクラスB
Window情報を複数持っているデータクラス
using System.Windows;
namespace 入れ子_ネスト_にしたデータクラスによる参照値渡し
{
public class DataClassB
{
public String windowName { get; set; } = string.Empty;
public double windowWidth { get; set; }
public double windowHeight { get; set; }
public Point windwoLocation { get; set; }
}
}
}
データクラスA・Bを内部に持つデータクラスalphaを作成
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 入れ子_ネスト_にしたデータクラスによる参照値渡し
{
public class SuperDataClas_alpha
{
public SuperDataClas_alpha(DataClassA _dataClassA, DataClassB _dataClassB)
{
//NullCheck
if (_dataClassA == null || _dataClassB == null)
{
throw new ArgumentNullException(nameof(_dataClassA));
}
else
{
dataclassA = _dataClassA;
dataclassB = _dataClassB;
}
}
public DataClassA dataclassA { get; set; }
public DataClassB dataclassB { get; set; }
}
}
UI画面
※貼るのが面倒なのでGitを参考にしてください。
DisplayA,DisplayBに各プロパティを変更する処理。
Git
git clone https://github.com/Sheephuman/nested_data_classes_passing.git
DisplayA
<Window x:Class="入れ子_ネスト_にしたデータクラスによる参照値渡し.DisplayWindowA"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:入れ子_ネスト_にしたデータクラスによる参照値渡し"
mc:Ignorable="d"
Title="DisplauWindowA" Height="450" Width="800"
Loaded="Window_Loaded">
<Grid>
<StackPanel>
<!-- IsEnabled をトグルするボタン -->
<Button Name="btnToggleIsEnabled" Content="Toggle IsEnabled" Click="btnToggleIsEnabled_Click" Margin="10" />
<!-- IsVisible をトグルするボタン -->
<Button Name="btnToggleIsVisible" Content="Toggle IsVisible" Click="btnToggleIsVisible_Click" Margin="10" />
<!-- WindowState (Maximize) をトグルするボタン -->
<Button Name="btnToggleWindowState" Content="Toggle Maximize" Click="btnToggleWindowState_Click" Margin="10" />
<!-- Topmost をトグルするボタン -->
<Button Name="btnToggleTopmost" Content="Toggle Topmost" Click="btnToggleTopmost_Click" Margin="10" />
<StackPanel>
<!-- ボタンを3つ作成 -->
<Button Content="Red" Width="100" Height="40" Click="RedButton_Click"/>
<Button Content="Green" Width="100" Height="40" Click="GreenButton_Click"/>
<Button Content="Blue" Width="100" Height="40" Click="AliceBlueButton_Click"/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
using System.Windows;
using System.Windows.Media;
namespace 入れ子_ネスト_にしたデータクラスによる参照値渡し
{
/// <summary>
/// DisplauWindowA.xaml の相互作用ロジック
/// </summary>
public partial class DisplayWindowA : Window
{
public DisplayWindowA(SuperDataClas_alpha _alpha)
{
InitializeComponent();
if (_alpha == null)
{
throw new ArgumentNullException(nameof(_alpha));
}
else
alpha = _alpha;
}
public SuperDataClas_alpha alpha { get; set; }
//public DataClassA dataclassA { get; set; }
private void Window_Loaded(object sender, RoutedEventArgs e)
{
//エイリアスする
var dataImpl = alpha.dataclassA;
dataImpl.StateEnabled = this.IsEnabled;
dataImpl.StateVisiblity = this.IsVisible;
dataImpl.StateMaximamize = WindowState == WindowState.Maximized;
dataImpl.StateTopMost = this.Topmost;
}
private void btnToggleWindowState_Click(object sender, RoutedEventArgs e)
{
var data = alpha.dataclassA;
// 最大化・通常サイズをトグル
if (this.WindowState == WindowState.Maximized)
{
this.WindowState = WindowState.Normal;
}
else
{
this.WindowState = WindowState.Maximized;
}
data.StateMaximamize = (this.WindowState == WindowState.Maximized);
}
private void btnToggleIsEnabled_Click(object sender, RoutedEventArgs e)
{
var data = alpha.dataclassA;
// IsEnabledをトグル
this.IsEnabled = !this.IsEnabled;
data.StateEnabled = this.IsEnabled;
}
private void btnToggleIsVisible_Click(object sender, RoutedEventArgs e)
{
var data = alpha.dataclassA;
// IsVisibleをトグル
this.Visibility = this.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
data.StateVisiblity = this.Visibility == Visibility.Visible;
}
private void btnToggleTopmost_Click(object sender, RoutedEventArgs e)
{
var data = alpha.dataclassA;
// Topmostをトグル
this.Topmost = !this.Topmost;
data.StateTopMost = this.Topmost;
}
private void RedButton_Click(object sender, RoutedEventArgs e)
{
var data = alpha.dataclassA;
this.Background = new SolidColorBrush(Colors.Red); // 背景色を赤に変更
data.WindowBackColor = Background;
}
private void GreenButton_Click(object sender, RoutedEventArgs e)
{
var data = alpha.dataclassA;
this.Background = new SolidColorBrush(Colors.Green); // 背景色を緑に変更
data.WindowBackColor = Background;
}
private void AliceBlueButton_Click(object sender, RoutedEventArgs e)
{
var data = alpha.dataclassA;
this.Background = new SolidColorBrush(Colors.AliceBlue); // 背景色をアリスブルーに変更
data.WindowBackColor = Background;
}
}
}
displayB
<Window x:Class="入れ子_ネスト_にしたデータクラスによる参照値渡し.DisplayWindowB"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:入れ子_ネスト_にしたデータクラスによる参照値渡し"
mc:Ignorable="d"
Title="DisplayWindowB" Height="450" Width="800">
<Grid>
<StackPanel>
<!-- テキストボックスで新しいタイトルを入力 -->
<TextBox Name="txtNewTitle" Width="300" Margin="10" />
<!-- タイトル変更ボタン -->
<Button Name="btnChangeTitle" Content="Change Title" Click="btnChangeTitle_Click" Margin="10" />
<!-- 現在のウィンドウタイトルを表示 -->
<TextBlock Name="txtCurrentTitle" Text="Current Title: Window" Margin="10" />
</StackPanel>
</Grid>
</Window>
using System.Windows;
namespace 入れ子_ネスト_にしたデータクラスによる参照値渡し
{
/// <summary>
/// DisplauWindowB.xaml の相互作用ロジック
/// </summary>
public partial class DisplayWindowB : Window
{
public DisplayWindowB(SuperDataClas_alpha _alpha)
{
InitializeComponent();
if (_alpha != null)
alpha = _alpha;
else
{
throw new ArgumentNullException(nameof(_alpha));
}
}
//public DataClassB DataClassB { get; set; }
public SuperDataClas_alpha alpha { get; set; }
private void btnChangeTitle_Click(object sender, RoutedEventArgs e)
{
// 新しいタイトルをテキストボックスから取得
string newTitle = txtNewTitle.Text;
// 新しいタイトルをウィンドウのタイトルとして設定
if (!string.IsNullOrEmpty(newTitle))
{
this.Title = newTitle;
txtCurrentTitle.Text = "Current Title: " + this.Title;
alpha.dataclassB.windowName = Title;
}
else
{
MessageBox.Show("Please enter a title.");
}
}
}
}
<Window x:Class="入れ子_ネスト_にしたデータクラスによる参照値渡し.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:入れ子_ネスト_にしたデータクラスによる参照値渡し"
mc:Ignorable="d"
Title="MainWindow" Height="250" Width="400">
<Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<Button x:Name="GetDataA_Button" Content="DataAを取得" Width=" 150" Height="50"
Margin=" 10" Click="GetDataA_Button_Click" />
<Button x:Name="GetDataB_Button"
Click="GetDataB_Button_Click"
Content="DataBを取得" Width=" 150" Height="50"
/>
</StackPanel>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Button x:Name="DataASendA_Button" Content="DataAをDisplayBに反映" Width=" 150" Height="50"
Margin=" 10" Click="DataAlphaSendtoDispB_Button_Click" />
<Button x:Name="GetDataBSend_Button"
Click="Data_AlphaSendtoDispA_Button_Click"
Content="DataBをDisplayAに反映" Width=" 150" Height="50"
/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
実際にデータクラスalphaを参照渡しする
下位のデータクラスが受け渡しされるかどうかを検証
ChatGPTによれば受け渡されるらしいけど、実証派なので
ボタン処理を二つ分書いてます。
private void DataAlphaSendtoDispB_Button_Click(object sender, RoutedEventArgs e)
{
//上位データクラス DataClassによる参照値渡し
dispB.alpha = alpha;
//SuperDataClas_alpha のインスタンスを参照渡しすることで、その内部の DataClassA や DataClassB の変更が即時に反映される
//ここではデータクラスAの内容が受け渡される
//各プロパティに反映させる
var dataA = alpha.dataclassA;
if(dataA.StateMaximamize)
dispB.WindowState = WindowState.Maximized;
if (dataA.StateVisiblity)
dispB.Visibility = Visibility.Visible;
else
dispB.Visibility = Visibility.Collapsed;
dispB.Topmost = dataA.StateTopMost;
dispB.IsEnabled = dataA.StateEnabled;
dispB.Background = dataA.WindowBackColor;
}
Data_AlphaSendtoDispA_Button_Clic
private void Data_AlphaSendtoDispA_Button_Click(object sender, RoutedEventArgs e)
{
//上位データクラス DataClassによる参照値渡し
dispA.alpha = alpha;
//SuperDataClas_alpha のインスタンスを参照渡しすることで、その内部の DataClassA や DataClassB の変更が即時に反映される
//ここではデータクラスBの内容が受け渡される
//各プロパティに反映させる
var dataB = alpha.dataclassB;
dispA.Left = dataB.windwoLocation.X;
dispA.Top = dataB.windwoLocation.Y;
dispA.Title = dataB.windowName;
dispA.Height = dataB.windowHeight;
dispA.Width = dataB.windowWidth;
}
ちなみに、データクラスalphaでGetDataメソッドとか何か処理したりするとデータクラスの数だけ条件分岐が必要になったりして保守性が落ちます。善し悪しですね。
あくまでアクセスだけに留めるべきかもしれません
interfaceを適切に使用すると条件分岐を回避できますが、今度はネスト構造が無意味になる(笑)。
実行画面スクショ(色変更だけ)
Data取得ボタンは要らないみたいですはい。
※直接貼ると圧縮されてしまうっぽい
入れ子データクラスが有用なケース
冗長さ回避のためリンクに
GPTで生成してもらいました。
入れ子データクラスが不適切なケース
1.入れ子構造のオブジェクトが変更された際に、それを外部に通知する仕組みが必要になる。
2.WPF の INotifyPropertyChanged を実装する場合、入れ子の各プロパティに PropertyChanged を伝播させる必要があり、実装が煩雑になる。
3. 深いネストによるパフォーマンス劣化
→ 深い入れ子構造を持つと、オブジェクトのシリアライズやデシリアライズ時のコストが増加。
4.再利用性が低下する
→ DataClassA や DataClassB を別のコンテキストで使いたい場合、入れ子構造のせいで SuperDataClass に依存してしまう。
→ DataClassA だけ使いたいのに SuperDataClass を経由しなければならない。
5.メモリリークのリスク
→ WPF で SuperDataClass を DataContext にバインドすると、入れ子のオブジェクトがメモリから解放されにくくなる。
→GPTの生成テキストのため未確認
→Disposeパターンの導入
【宣伝】
自作のはるあこんばーた(Twitter用に動画変換するやつ)によるGIF作成をしてます。
BugFixしたので近日中に上げます。
あとがき
上位のデータクラスを参照値渡しすると、下位のデータクラスも反映されることが確認されました。
案外便利だと思いますがサンプルコードの実装めんどくさかったです(笑)。
試行錯誤しながら書いたんで冗長かつ実質不要な処理もある気がします。
時間があるときに書き直します。
良いと思ったら高評価、チャンネル登録、いいね、バッドボタン(低評価)、ストック、愛のある女性的なコメントをお願いします。