質問するログイン新規登録

回答編集履歴

2

見直しキャンペーン中

2023/07/23 06:17

投稿

TN8001
TN8001

スコア10180

answer CHANGED
@@ -1,310 +1,308 @@
1
- 過去の回答の使いまわしなので、例としてはちょっと重めですが
2
- ```xaml
3
- <!-- 直置きパターン -->
4
- <local:TreeUserControl DataContext="{Binding Tree}" />
5
- <!-- DataTemplateパターン -->
6
- <ContentPresenter Grid.Column="1" Content="{Binding Tree}" />
7
- ```
8
- この2つの違いと、(結果的には何も変わらないのですが^^; 初めて見たときは「はぁ~なるほど!」と思いました)
9
- `TreeViewModel`・`TreeUserControl`の関係を見てください。
10
-
11
- `TreeUserControl`が`TreeView`だけなのは、ちょっともったいない?かもしれません。
12
- ボタン類も一緒に入れると、責務がきれいに分かれたようになると思います。
13
-
14
- `MainViewModel`が`TreeViewModel`を保持しているので、`MainViewModel`からツリーの状態はいつでも見れます。
15
-
16
- `DependencyProperty`は特に出番はありませんでした。
17
- 左右は同じ`TreeViewModel`を見ているので、自動的に連動します。
18
- これでは意味がないですが、チェックされてるものだけ表示する(`TreeView`だと大変すぎるが^^; `ListBox`なんかは簡単)等、差をつけるときに`DependencyProperty`があれば便利です。
19
-
20
- MainWindow.xaml
21
- ```xaml
22
- <Window
23
- x:Class="Questions292544.MainWindow"
24
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
25
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
26
- xmlns:local="clr-namespace:Questions292544"
27
- Width="800"
28
- Height="450">
29
- <Window.DataContext>
30
- <local:MainViewModel />
31
- </Window.DataContext>
32
- <Window.Resources>
33
- <DataTemplate DataType="{x:Type local:TreeViewModel}">
34
- <local:TreeUserControl />
35
- </DataTemplate>
36
- <Style TargetType="Button">
37
- <Setter Property="Margin" Value="5" />
38
- </Style>
39
- </Window.Resources>
40
- <DockPanel>
41
- <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
42
- <Button Command="{Binding Tree.AllCheckCmd}" Content="全チェック" />
43
- <Button Command="{Binding Tree.AllUncheckCmd}" Content="全アンチェック" />
44
- <Button Command="{Binding Tree.AllExpandCmd}" Content="全展開" />
45
- <Button Command="{Binding Tree.AllContractCmd}" Content="全畳む" />
46
- <Button Command="{Binding Tree.AddNodeCmd}" Content="ノード追加" />
47
- <Button Command="{Binding PrintCheckedNodesCmd}" Content="チェックノード表示(Debug)" />
48
- </StackPanel>
49
- <Grid>
50
- <Grid.ColumnDefinitions>
51
- <ColumnDefinition />
52
- <ColumnDefinition />
53
- </Grid.ColumnDefinitions>
54
- <!-- 直置きパターン -->
55
- <local:TreeUserControl DataContext="{Binding Tree}" />
56
- <!-- DataTemplateパターン -->
57
- <ContentPresenter Grid.Column="1" Content="{Binding Tree}" />
58
- </Grid>
59
- </DockPanel>
60
- </Window>
61
- ```
62
-
63
- TreeUserControl.xaml
64
- ```xaml
65
- <UserControl
66
- x:Class="Questions292544.TreeUserControl"
67
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
68
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
69
- xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
70
- xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
71
- d:DesignHeight="450"
72
- d:DesignWidth="800"
73
- mc:Ignorable="d">
74
- <TreeView ItemsSource="{Binding Root.Children}">
75
- <TreeView.ItemTemplate>
76
- <HierarchicalDataTemplate ItemsSource="{Binding Children}">
77
- <StackPanel Orientation="Horizontal">
78
- <CheckBox
79
- Margin="5"
80
- VerticalContentAlignment="Center"
81
- IsChecked="{Binding IsChecked}" />
82
- <TextBlock Margin="5" Text="{Binding Name}" />
83
- <Button
84
- Margin="5"
85
- Command="{Binding DataContext.DeleteNodeCmd, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeView}}}"
86
- CommandParameter="{Binding}"
87
- Content="削除" />
88
- </StackPanel>
89
- </HierarchicalDataTemplate>
90
- </TreeView.ItemTemplate>
91
- <TreeView.ItemContainerStyle>
92
- <Style TargetType="{x:Type TreeViewItem}">
93
- <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
94
- <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
95
- </Style>
96
- </TreeView.ItemContainerStyle>
97
- </TreeView>
98
- </UserControl>
99
- ```
100
-
101
- 他コード
102
- ```C#
103
- using System;
104
- using System.Collections;
105
- using System.Collections.Generic;
106
- using System.Collections.ObjectModel;
107
- using System.ComponentModel;
108
- using System.Diagnostics;
109
- using System.Linq;
110
- using System.Runtime.CompilerServices;
111
- using System.Windows.Input;
112
-
113
- namespace Questions292544
114
- {
115
- class MainViewModel : Observable
116
- {
117
- public TreeViewModel Tree { get; } = new TreeViewModel();
118
- public RelayCommand PrintCheckedNodesCmd { get; }
119
- public MainViewModel()
120
- {
121
- PrintCheckedNodesCmd = new RelayCommand(() =>
122
- {
123
- Debug.WriteLine("IsChecked"); // チェックを入れた最上位の情報表示
124
-
125
- foreach(var c in Tree.Root.Children) Dfs(c);
126
-
127
- void Dfs(TreeNode node) // 深さ優先探索
128
- {
129
- if(node.IsChecked == true)
130
- {
131
- Debug.WriteLine(node.Name);
132
- return;
133
- }
134
- foreach(var c in node.Children) Dfs(c);
135
- }
136
- });
137
- }
138
- }
139
-
140
- class TreeViewModel : Observable
141
- {
142
- public TreeNode Root { get; }
143
-
144
- public RelayCommand AllCheckCmd { get; }
145
- public RelayCommand AllUncheckCmd { get; }
146
- public RelayCommand AllExpandCmd { get; }
147
- public RelayCommand AllContractCmd { get; }
148
- public RelayCommand<TreeNode> DeleteNodeCmd { get; }
149
- public RelayCommand AddNodeCmd { get; }
150
-
151
- public TreeViewModel()
152
- {
153
- Root = new TreeNode("Root") {
154
- new TreeNode("Node1") {
155
- new TreeNode("Node1-1"),
156
- new TreeNode("Node1-2") {
157
- new TreeNode("Node1-2-1"),
158
- new TreeNode("Node1-2-2"),
159
- },
160
- },
161
- new TreeNode("Node2") {
162
- new TreeNode("Node2-1") {
163
- new TreeNode("Node2-1-1"),
164
- new TreeNode("Node2-1-2"),
165
- },
166
- },
167
- };
168
-
169
- AllCheckCmd = new RelayCommand(() => { foreach(var c in Root) c.IsChecked = true; });
170
- AllUncheckCmd = new RelayCommand(() => { foreach(var c in Root) c.IsChecked = false; });
171
- AllExpandCmd = new RelayCommand(() => { foreach(var c in Root) c.IsExpanded = true; });
172
- AllContractCmd = new RelayCommand(() => { foreach(var c in Root) c.IsExpanded = false; });
173
- DeleteNodeCmd = new RelayCommand<TreeNode>((c) => c.Parent.Remove(c));
174
- AddNodeCmd = new RelayCommand(() =>
175
- {
176
- // 選択の子に追加
177
- if(Root.FirstOrDefault(x => x.IsSelected) is TreeNode selectedItem)
178
- selectedItem.Add(new TreeNode("NewNode"));
179
- else Root.Add(new TreeNode("NewNode"));
180
- });
181
- }
182
- }
183
-
184
- class TreeNode : Observable, IEnumerable<TreeNode>
185
- {
186
- string _Name;
187
- public string Name { get => _Name; set => Set(ref _Name, value); }
188
-
189
- bool _IsExpanded;
190
- public bool IsExpanded { get => _IsExpanded; set => Set(ref _IsExpanded, value); }
191
-
192
- bool _IsSelected;
193
- public bool IsSelected { get => _IsSelected; set => Set(ref _IsSelected, value); }
194
-
195
- bool? _IsChecked = false;
196
- public bool? IsChecked // ThreeState
197
- {
198
- get => _IsChecked;
199
- set
200
- {
201
- if(_IsChecked == value) return;
202
- if(reentrancyCheck) return; // 再入防止
203
- reentrancyCheck = true;
204
- Set(ref _IsChecked, value);
205
- UpdateCheckState(); // チェックが変わったら上下も更新
206
- reentrancyCheck = false;
207
- }
208
- }
209
-
210
- public ObservableCollection<TreeNode> Children { get; } = new ObservableCollection<TreeNode>();
211
-
212
- public TreeNode Parent { get; private set; } // 追加・削除の簡便さのため親ノードが欲しい
213
-
214
- bool reentrancyCheck;
215
-
216
- public TreeNode(string name) => Name = name;
217
-
218
- public void Add(TreeNode child)
219
- {
220
- child.Parent = this;
221
- Children.Add(child);
222
- IsExpanded = true;
223
- child.UpdateCheckState();
224
- }
225
- public void Remove(TreeNode child)
226
- {
227
- Children.Remove(child);
228
- child.Parent = null;
229
- if(0 < Children.Count) IsChecked = DetermineCheckState();
230
- }
231
-
232
- //チェック状態反映
233
- // https://docs.telerik.com/devtools/wpf/controls/radtreeview/how-to/howto-tri-state-mvvm
234
- void UpdateCheckState()
235
- {
236
- if(0 < Children.Count) UpdateChildrenCheckState();
237
- if(Parent != null) Parent.IsChecked = Parent.DetermineCheckState();
238
- }
239
- void UpdateChildrenCheckState()
240
- {
241
- foreach(var c in Children)
242
- {
243
- if(IsChecked != null) c.IsChecked = IsChecked;
244
- }
245
- }
246
- bool? DetermineCheckState()
247
- {
248
- var checkCount = Children.Count(x => x.IsChecked == true);
249
- if(checkCount == Children.Count) return true;
250
-
251
- var uncheckCount = Children.Count(x => x.IsChecked == false);
252
- if(uncheckCount == Children.Count) return false;
253
-
254
- return null;
255
- }
256
-
257
- // 子孫を全列挙(Children列挙でないので注意)
258
- IEnumerable<TreeNode> Descendants(TreeNode node) => node.Children.Concat(node.Children.SelectMany(Descendants));
259
- public IEnumerator<TreeNode> GetEnumerator() => Descendants(this).GetEnumerator();
260
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
261
- }
262
-
263
- class Observable : INotifyPropertyChanged
264
- {
265
- public event PropertyChangedEventHandler PropertyChanged;
266
- protected void Set<T>(ref T storage, T value, [CallerMemberName] string name = null)
267
- {
268
- if(Equals(storage, value)) return;
269
- storage = value;
270
- OnPropertyChanged(name);
271
- }
272
- protected void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
273
- }
274
- class RelayCommand : ICommand
275
- {
276
- readonly Action exec;
277
- readonly Func<bool> can;
278
- public event EventHandler CanExecuteChanged;
279
- public RelayCommand(Action e) : this(e, null) { }
280
- public RelayCommand(Action e, Func<bool> c)
281
- {
282
- exec = e ?? throw new ArgumentNullException(nameof(e));
283
- can = c;
284
- }
285
- public bool CanExecute(object _) => can == null || can();
286
- public void Execute(object _) => exec();
287
- public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
288
- }
289
- class RelayCommand<T> : ICommand
290
- {
291
- readonly Action<T> exec;
292
- readonly Func<T, bool> can;
293
- public event EventHandler CanExecuteChanged;
294
- public RelayCommand(Action<T> e) : this(e, null) { }
295
- public RelayCommand(Action<T> e, Func<T, bool> c)
296
- {
297
- exec = e ?? throw new ArgumentNullException(nameof(e));
298
- can = c;
299
- }
300
- public bool CanExecute(object p) => can == null || can((T)p);
301
- public void Execute(object p) => exec((T)p);
302
- public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
303
- }
304
- }
305
- ```
306
- .xaml.csは初期状態
307
-
308
- ---
309
-
1
+ 過去の回答の使いまわしなので、例としてはちょっと重めですが
2
+ ```xml
3
+ <!-- 直置きパターン -->
4
+ <local:TreeUserControl DataContext="{Binding Tree}" />
5
+ <!-- DataTemplateパターン -->
6
+ <ContentPresenter Grid.Column="1" Content="{Binding Tree}" />
7
+ ```
8
+ この2つの違いと、(結果的には何も変わらないのですが^^; 初めて見たときは「はぁ~なるほど!」と思いました)
9
+ `TreeViewModel`・`TreeUserControl`の関係を見てください。
10
+
11
+ `TreeUserControl`が`TreeView`だけなのは、ちょっともったいない?かもしれません。
12
+ ボタン類も一緒に入れると、責務がきれいに分かれたようになると思います。
13
+
14
+ `MainViewModel`が`TreeViewModel`を保持しているので、`MainViewModel`からツリーの状態はいつでも見れます。
15
+
16
+ `DependencyProperty`は特に出番はありませんでした。
17
+ 左右は同じ`TreeViewModel`を見ているので、自動的に連動します。
18
+ これでは意味がないですが、チェックされてるものだけ表示する(`TreeView`だと大変すぎるが^^; `ListBox`なんかは簡単)等、差をつけるときに`DependencyProperty`があれば便利です。
19
+
20
+ ```xml:MainWindow.xaml
21
+ <Window
22
+ x:Class="Questions292544.MainWindow"
23
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
24
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
25
+ xmlns:local="clr-namespace:Questions292544"
26
+ Width="800"
27
+ Height="450">
28
+ <Window.DataContext>
29
+ <local:MainViewModel />
30
+ </Window.DataContext>
31
+ <Window.Resources>
32
+ <DataTemplate DataType="{x:Type local:TreeViewModel}">
33
+ <local:TreeUserControl />
34
+ </DataTemplate>
35
+ <Style TargetType="Button">
36
+ <Setter Property="Margin" Value="5" />
37
+ </Style>
38
+ </Window.Resources>
39
+ <DockPanel>
40
+ <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
41
+ <Button Command="{Binding Tree.AllCheckCmd}" Content="全チェック" />
42
+ <Button Command="{Binding Tree.AllUncheckCmd}" Content="全アンチェック" />
43
+ <Button Command="{Binding Tree.AllExpandCmd}" Content="全展開" />
44
+ <Button Command="{Binding Tree.AllContractCmd}" Content="全畳む" />
45
+ <Button Command="{Binding Tree.AddNodeCmd}" Content="ノード追加" />
46
+ <Button Command="{Binding PrintCheckedNodesCmd}" Content="チェックノード表示(Debug)" />
47
+ </StackPanel>
48
+ <Grid>
49
+ <Grid.ColumnDefinitions>
50
+ <ColumnDefinition />
51
+ <ColumnDefinition />
52
+ </Grid.ColumnDefinitions>
53
+ <!-- 直置きパターン -->
54
+ <local:TreeUserControl DataContext="{Binding Tree}" />
55
+ <!-- DataTemplateパターン -->
56
+ <ContentPresenter Grid.Column="1" Content="{Binding Tree}" />
57
+ </Grid>
58
+ </DockPanel>
59
+ </Window>
60
+ ```
61
+
62
+ ```xml:TreeUserControl.xaml
63
+ <UserControl
64
+ x:Class="Questions292544.TreeUserControl"
65
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
66
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
67
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
68
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
69
+ d:DesignHeight="450"
70
+ d:DesignWidth="800"
71
+ mc:Ignorable="d">
72
+ <TreeView ItemsSource="{Binding Root.Children}">
73
+ <TreeView.ItemTemplate>
74
+ <HierarchicalDataTemplate ItemsSource="{Binding Children}">
75
+ <StackPanel Orientation="Horizontal">
76
+ <CheckBox
77
+ Margin="5"
78
+ VerticalContentAlignment="Center"
79
+ IsChecked="{Binding IsChecked}" />
80
+ <TextBlock Margin="5" Text="{Binding Name}" />
81
+ <Button
82
+ Margin="5"
83
+ Command="{Binding DataContext.DeleteNodeCmd, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeView}}}"
84
+ CommandParameter="{Binding}"
85
+ Content="削除" />
86
+ </StackPanel>
87
+ </HierarchicalDataTemplate>
88
+ </TreeView.ItemTemplate>
89
+ <TreeView.ItemContainerStyle>
90
+ <Style TargetType="{x:Type TreeViewItem}">
91
+ <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
92
+ <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
93
+ </Style>
94
+ </TreeView.ItemContainerStyle>
95
+ </TreeView>
96
+ </UserControl>
97
+ ```
98
+
99
+ 他コード
100
+ ```cs
101
+ using System;
102
+ using System.Collections;
103
+ using System.Collections.Generic;
104
+ using System.Collections.ObjectModel;
105
+ using System.ComponentModel;
106
+ using System.Diagnostics;
107
+ using System.Linq;
108
+ using System.Runtime.CompilerServices;
109
+ using System.Windows.Input;
110
+
111
+ namespace Questions292544
112
+ {
113
+ class MainViewModel : Observable
114
+ {
115
+ public TreeViewModel Tree { get; } = new TreeViewModel();
116
+ public RelayCommand PrintCheckedNodesCmd { get; }
117
+ public MainViewModel()
118
+ {
119
+ PrintCheckedNodesCmd = new RelayCommand(() =>
120
+ {
121
+ Debug.WriteLine("IsChecked"); // チェックを入れた最上位の情報表示
122
+
123
+ foreach(var c in Tree.Root.Children) Dfs(c);
124
+
125
+ void Dfs(TreeNode node) // 深さ優先探索
126
+ {
127
+ if(node.IsChecked == true)
128
+ {
129
+ Debug.WriteLine(node.Name);
130
+ return;
131
+ }
132
+ foreach(var c in node.Children) Dfs(c);
133
+ }
134
+ });
135
+ }
136
+ }
137
+
138
+ class TreeViewModel : Observable
139
+ {
140
+ public TreeNode Root { get; }
141
+
142
+ public RelayCommand AllCheckCmd { get; }
143
+ public RelayCommand AllUncheckCmd { get; }
144
+ public RelayCommand AllExpandCmd { get; }
145
+ public RelayCommand AllContractCmd { get; }
146
+ public RelayCommand<TreeNode> DeleteNodeCmd { get; }
147
+ public RelayCommand AddNodeCmd { get; }
148
+
149
+ public TreeViewModel()
150
+ {
151
+ Root = new TreeNode("Root") {
152
+ new TreeNode("Node1") {
153
+ new TreeNode("Node1-1"),
154
+ new TreeNode("Node1-2") {
155
+ new TreeNode("Node1-2-1"),
156
+ new TreeNode("Node1-2-2"),
157
+ },
158
+ },
159
+ new TreeNode("Node2") {
160
+ new TreeNode("Node2-1") {
161
+ new TreeNode("Node2-1-1"),
162
+ new TreeNode("Node2-1-2"),
163
+ },
164
+ },
165
+ };
166
+
167
+ AllCheckCmd = new RelayCommand(() => { foreach(var c in Root) c.IsChecked = true; });
168
+ AllUncheckCmd = new RelayCommand(() => { foreach(var c in Root) c.IsChecked = false; });
169
+ AllExpandCmd = new RelayCommand(() => { foreach(var c in Root) c.IsExpanded = true; });
170
+ AllContractCmd = new RelayCommand(() => { foreach(var c in Root) c.IsExpanded = false; });
171
+ DeleteNodeCmd = new RelayCommand<TreeNode>((c) => c.Parent.Remove(c));
172
+ AddNodeCmd = new RelayCommand(() =>
173
+ {
174
+ // 選択の子に追加
175
+ if(Root.FirstOrDefault(x => x.IsSelected) is TreeNode selectedItem)
176
+ selectedItem.Add(new TreeNode("NewNode"));
177
+ else Root.Add(new TreeNode("NewNode"));
178
+ });
179
+ }
180
+ }
181
+
182
+ class TreeNode : Observable, IEnumerable<TreeNode>
183
+ {
184
+ string _Name;
185
+ public string Name { get => _Name; set => Set(ref _Name, value); }
186
+
187
+ bool _IsExpanded;
188
+ public bool IsExpanded { get => _IsExpanded; set => Set(ref _IsExpanded, value); }
189
+
190
+ bool _IsSelected;
191
+ public bool IsSelected { get => _IsSelected; set => Set(ref _IsSelected, value); }
192
+
193
+ bool? _IsChecked = false;
194
+ public bool? IsChecked // ThreeState
195
+ {
196
+ get => _IsChecked;
197
+ set
198
+ {
199
+ if(_IsChecked == value) return;
200
+ if(reentrancyCheck) return; // 再入防止
201
+ reentrancyCheck = true;
202
+ Set(ref _IsChecked, value);
203
+ UpdateCheckState(); // チェックが変わったら上下も更新
204
+ reentrancyCheck = false;
205
+ }
206
+ }
207
+
208
+ public ObservableCollection<TreeNode> Children { get; } = new ObservableCollection<TreeNode>();
209
+
210
+ public TreeNode Parent { get; private set; } // 追加・削除の簡便さのため親ノードが欲しい
211
+
212
+ bool reentrancyCheck;
213
+
214
+ public TreeNode(string name) => Name = name;
215
+
216
+ public void Add(TreeNode child)
217
+ {
218
+ child.Parent = this;
219
+ Children.Add(child);
220
+ IsExpanded = true;
221
+ child.UpdateCheckState();
222
+ }
223
+ public void Remove(TreeNode child)
224
+ {
225
+ Children.Remove(child);
226
+ child.Parent = null;
227
+ if(0 < Children.Count) IsChecked = DetermineCheckState();
228
+ }
229
+
230
+ //チェック状態反映
231
+ // https://docs.telerik.com/devtools/wpf/controls/radtreeview/how-to/howto-tri-state-mvvm
232
+ void UpdateCheckState()
233
+ {
234
+ if(0 < Children.Count) UpdateChildrenCheckState();
235
+ if(Parent != null) Parent.IsChecked = Parent.DetermineCheckState();
236
+ }
237
+ void UpdateChildrenCheckState()
238
+ {
239
+ foreach(var c in Children)
240
+ {
241
+ if(IsChecked != null) c.IsChecked = IsChecked;
242
+ }
243
+ }
244
+ bool? DetermineCheckState()
245
+ {
246
+ var checkCount = Children.Count(x => x.IsChecked == true);
247
+ if(checkCount == Children.Count) return true;
248
+
249
+ var uncheckCount = Children.Count(x => x.IsChecked == false);
250
+ if(uncheckCount == Children.Count) return false;
251
+
252
+ return null;
253
+ }
254
+
255
+ // 子孫を全列挙(Children列挙でないので注意)
256
+ IEnumerable<TreeNode> Descendants(TreeNode node) => node.Children.Concat(node.Children.SelectMany(Descendants));
257
+ public IEnumerator<TreeNode> GetEnumerator() => Descendants(this).GetEnumerator();
258
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
259
+ }
260
+
261
+ class Observable : INotifyPropertyChanged
262
+ {
263
+ public event PropertyChangedEventHandler PropertyChanged;
264
+ protected void Set<T>(ref T storage, T value, [CallerMemberName] string name = null)
265
+ {
266
+ if(Equals(storage, value)) return;
267
+ storage = value;
268
+ OnPropertyChanged(name);
269
+ }
270
+ protected void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
271
+ }
272
+ class RelayCommand : ICommand
273
+ {
274
+ readonly Action exec;
275
+ readonly Func<bool> can;
276
+ public event EventHandler CanExecuteChanged;
277
+ public RelayCommand(Action e) : this(e, null) { }
278
+ public RelayCommand(Action e, Func<bool> c)
279
+ {
280
+ exec = e ?? throw new ArgumentNullException(nameof(e));
281
+ can = c;
282
+ }
283
+ public bool CanExecute(object _) => can == null || can();
284
+ public void Execute(object _) => exec();
285
+ public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
286
+ }
287
+ class RelayCommand<T> : ICommand
288
+ {
289
+ readonly Action<T> exec;
290
+ readonly Func<T, bool> can;
291
+ public event EventHandler CanExecuteChanged;
292
+ public RelayCommand(Action<T> e) : this(e, null) { }
293
+ public RelayCommand(Action<T> e, Func<T, bool> c)
294
+ {
295
+ exec = e ?? throw new ArgumentNullException(nameof(e));
296
+ can = c;
297
+ }
298
+ public bool CanExecute(object p) => can == null || can((T)p);
299
+ public void Execute(object p) => exec((T)p);
300
+ public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
301
+ }
302
+ }
303
+ ```
304
+ .xaml.csは初期状態
305
+
306
+ ---
307
+
310
308
  `TreeNode`は厳密には`ViewModel`になるかもしれませんが、ライブラリなしではめんどくさいのでVM・M兼用です^^;

1

全面書き直し

2020/09/20 02:14

投稿

TN8001
TN8001

スコア10180

answer CHANGED
@@ -1,250 +1,310 @@
1
+ 過去の回答の使いまわしなので、例としてはちょっと重めですが
2
+ ```xaml
3
+ <!-- 直置きパターン -->
4
+ <local:TreeUserControl DataContext="{Binding Tree}" />
5
+ <!-- DataTemplateパターン -->
6
+ <ContentPresenter Grid.Column="1" Content="{Binding Tree}" />
7
+ ```
8
+ この2つの違いと、(結果的には何も変わらないのですが^^; 初めて見たときは「はぁ~なるほど!」と思いました)
1
- > UserControlがPrism的な部分ビュー
9
+ `TreeViewModel`・`TreeUserControl`の関係を見てください。
2
10
 
3
- の話になります。ToDoアプリっぽいものを例題にしました。
4
- `MainViewModel``TodoViewModel`→`TaskViewModel`関係が、いかに`View`に展開されるかを確認てください
11
+ `TreeUserControl``TreeView`だけなちょっともったない?れません
5
- `Model`部分は手抜きなので重要ではありせん^^;
12
+ ボタン類も一緒に入れると、責務がれいに分かれたようにると思いす。
6
13
 
7
- [NuGet Gallery | ReactiveProperty.WPF 7.3.0](https://www.nuget.org/packages/ReactiveProperty.WPF)必要になります。
14
+ `MainViewModel``TreeViewModel`を保持しているので、`MainViewModel`からツリーの状態はいつでも見れます。
8
15
 
16
+ `DependencyProperty`は特に出番はありませんでした。
17
+ 左右は同じ`TreeViewModel`を見ているので、自動的に連動します。
18
+ これでは意味がないですが、チェックされてるものだけ表示する(`TreeView`だと大変すぎるが^^; `ListBox`なんかは簡単)等、差をつけるときに`DependencyProperty`があれば便利です。
19
+
9
20
  MainWindow.xaml
10
21
  ```xaml
11
22
  <Window
12
23
  x:Class="Questions292544.MainWindow"
13
24
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
14
25
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
15
- xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
16
26
  xmlns:local="clr-namespace:Questions292544"
17
- xmlns:rp="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
18
27
  Width="800"
19
28
  Height="450">
20
- <i:Interaction.Triggers>
21
- <i:EventTrigger EventName="Closing">
22
- <rp:EventToReactiveCommand Command="{Binding TodoViewModel.SaveCommand}" />
23
- </i:EventTrigger>
24
- </i:Interaction.Triggers>
25
29
  <Window.DataContext>
26
30
  <local:MainViewModel />
27
31
  </Window.DataContext>
28
32
  <Window.Resources>
29
- <DataTemplate DataType="{x:Type local:TodoViewModel}">
33
+ <DataTemplate DataType="{x:Type local:TreeViewModel}">
30
- <local:TodoView />
34
+ <local:TreeUserControl />
31
35
  </DataTemplate>
36
+ <Style TargetType="Button">
37
+ <Setter Property="Margin" Value="5" />
38
+ </Style>
32
39
  </Window.Resources>
33
40
  <DockPanel>
41
+ <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
42
+ <Button Command="{Binding Tree.AllCheckCmd}" Content="全チェック" />
43
+ <Button Command="{Binding Tree.AllUncheckCmd}" Content="全アンチェック" />
44
+ <Button Command="{Binding Tree.AllExpandCmd}" Content="全展開" />
45
+ <Button Command="{Binding Tree.AllContractCmd}" Content="全畳む" />
46
+ <Button Command="{Binding Tree.AddNodeCmd}" Content="ノード追加" />
47
+ <Button Command="{Binding PrintCheckedNodesCmd}" Content="チェックノード表示(Debug)" />
48
+ </StackPanel>
34
- <TextBlock
49
+ <Grid>
35
- HorizontalAlignment="Center"
50
+ <Grid.ColumnDefinitions>
36
- DockPanel.Dock="Top"
37
- Text="TodoList" />
51
+ <ColumnDefinition />
52
+ <ColumnDefinition />
53
+ </Grid.ColumnDefinitions>
54
+ <!-- 直置きパターン -->
38
- <ContentPresenter Content="{Binding TodoViewModel}" />
55
+ <local:TreeUserControl DataContext="{Binding Tree}" />
56
+ <!-- DataTemplateパターン -->
57
+ <ContentPresenter Grid.Column="1" Content="{Binding Tree}" />
58
+ </Grid>
39
59
  </DockPanel>
40
60
  </Window>
41
61
  ```
62
+
42
- MainViewModel.cs
63
+ TreeUserControl.xaml
64
+ ```xaml
65
+ <UserControl
66
+ x:Class="Questions292544.TreeUserControl"
67
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
68
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
69
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
70
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
71
+ d:DesignHeight="450"
72
+ d:DesignWidth="800"
73
+ mc:Ignorable="d">
74
+ <TreeView ItemsSource="{Binding Root.Children}">
75
+ <TreeView.ItemTemplate>
76
+ <HierarchicalDataTemplate ItemsSource="{Binding Children}">
77
+ <StackPanel Orientation="Horizontal">
78
+ <CheckBox
79
+ Margin="5"
80
+ VerticalContentAlignment="Center"
81
+ IsChecked="{Binding IsChecked}" />
82
+ <TextBlock Margin="5" Text="{Binding Name}" />
83
+ <Button
84
+ Margin="5"
85
+ Command="{Binding DataContext.DeleteNodeCmd, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeView}}}"
86
+ CommandParameter="{Binding}"
87
+ Content="削除" />
88
+ </StackPanel>
89
+ </HierarchicalDataTemplate>
90
+ </TreeView.ItemTemplate>
91
+ <TreeView.ItemContainerStyle>
92
+ <Style TargetType="{x:Type TreeViewItem}">
93
+ <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
94
+ <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
95
+ </Style>
96
+ </TreeView.ItemContainerStyle>
97
+ </TreeView>
98
+ </UserControl>
99
+ ```
100
+
101
+ 他コード
43
102
  ```C#
103
+ using System;
104
+ using System.Collections;
105
+ using System.Collections.Generic;
106
+ using System.Collections.ObjectModel;
44
107
  using System.ComponentModel;
108
+ using System.Diagnostics;
109
+ using System.Linq;
110
+ using System.Runtime.CompilerServices;
111
+ using System.Windows.Input;
45
112
 
46
113
  namespace Questions292544
47
114
  {
48
- internal class MainViewModel : INotifyPropertyChanged
115
+ class MainViewModel : Observable
49
- {
116
+ {
117
+ public TreeViewModel Tree { get; } = new TreeViewModel();
50
- public TodoViewModel TodoViewModel { get; }
118
+ public RelayCommand PrintCheckedNodesCmd { get; }
119
+ public MainViewModel()
120
+ {
121
+ PrintCheckedNodesCmd = new RelayCommand(() =>
122
+ {
123
+ Debug.WriteLine("IsChecked"); // チェックを入れた最上位の情報表示
51
124
 
52
- public MainViewModel() => TodoViewModel = new TodoViewModel(TodoModel.Load("test.xml"));
125
+ foreach(var c in Tree.Root.Children) Dfs(c);
53
126
 
54
- public event PropertyChangedEventHandler PropertyChanged;
127
+ void Dfs(TreeNode node) // 深さ優先探索
128
+ {
129
+ if(node.IsChecked == true)
130
+ {
131
+ Debug.WriteLine(node.Name);
132
+ return;
55
- }
133
+ }
134
+ foreach(var c in node.Children) Dfs(c);
56
- }
135
+ }
57
- ```
136
+ });
137
+ }
138
+ }
58
139
 
140
+ class TreeViewModel : Observable
59
- ---
141
+ {
142
+ public TreeNode Root { get; }
60
143
 
61
- TodoView.xaml
62
- ```xaml
63
- <UserControl
64
- x:Class="Questions292544.TodoView"
65
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
66
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
67
- xmlns:local="clr-namespace:Questions292544"
68
- MinWidth="200"
69
- MinHeight="200">
70
- <UserControl.Resources>
144
+ public RelayCommand AllCheckCmd { get; }
145
+ public RelayCommand AllUncheckCmd { get; }
146
+ public RelayCommand AllExpandCmd { get; }
147
+ public RelayCommand AllContractCmd { get; }
71
- <DataTemplate DataType="{x:Type local:TaskViewModel}">
148
+ public RelayCommand<TreeNode> DeleteNodeCmd { get; }
72
- <local:TaskView />
149
+ public RelayCommand AddNodeCmd { get; }
73
- </DataTemplate>
74
150
 
75
- <DataTemplate x:Key="TaskEditView" DataType="{x:Type local:TaskViewModel}">
76
- <StackPanel>
77
- <TextBox Text="{Binding Title.Value, UpdateSourceTrigger=PropertyChanged}" />
78
- </StackPanel>
151
+ public TreeViewModel()
79
- </DataTemplate>
152
+ {
80
- </UserControl.Resources>
153
+ Root = new TreeNode("Root") {
154
+ new TreeNode("Node1") {
155
+ new TreeNode("Node1-1"),
156
+ new TreeNode("Node1-2") {
157
+ new TreeNode("Node1-2-1"),
158
+ new TreeNode("Node1-2-2"),
159
+ },
160
+ },
161
+ new TreeNode("Node2") {
162
+ new TreeNode("Node2-1") {
163
+ new TreeNode("Node2-1-1"),
164
+ new TreeNode("Node2-1-2"),
165
+ },
166
+ },
167
+ };
81
168
 
169
+ AllCheckCmd = new RelayCommand(() => { foreach(var c in Root) c.IsChecked = true; });
170
+ AllUncheckCmd = new RelayCommand(() => { foreach(var c in Root) c.IsChecked = false; });
171
+ AllExpandCmd = new RelayCommand(() => { foreach(var c in Root) c.IsExpanded = true; });
172
+ AllContractCmd = new RelayCommand(() => { foreach(var c in Root) c.IsExpanded = false; });
173
+ DeleteNodeCmd = new RelayCommand<TreeNode>((c) => c.Parent.Remove(c));
174
+ AddNodeCmd = new RelayCommand(() =>
175
+ {
82
- <DockPanel>
176
+ // 選択の子に追加
177
+ if(Root.FirstOrDefault(x => x.IsSelected) is TreeNode selectedItem)
178
+ selectedItem.Add(new TreeNode("NewNode"));
83
- <DockPanel DockPanel.Dock="Bottom">
179
+ else Root.Add(new TreeNode("NewNode"));
84
- <Button Command="{Binding AddCommand}" Content="New Task" />
85
- <ContentPresenter Content="{Binding Selected.Value}" ContentTemplate="{StaticResource TaskEditView}" />
86
- </DockPanel>
180
+ });
181
+ }
182
+ }
87
183
 
88
- <ListBox ItemsSource="{Binding TaskList}" SelectedItem="{Binding Selected.Value}">
89
- <ListBox.ItemContainerStyle>
90
- <Style TargetType="ListBoxItem">
184
+ class TreeNode : Observable, IEnumerable<TreeNode>
91
- <Setter Property="HorizontalContentAlignment" Value="Stretch" />
92
- </Style>
93
- </ListBox.ItemContainerStyle>
94
- </ListBox>
95
- </DockPanel>
96
- </UserControl>
97
- ```
185
+ {
98
- TodoViewModel.cs
99
- ```C#
100
- using System;
186
+ string _Name;
101
- using System.Reactive.Linq;
102
- using Reactive.Bindings;
103
- using Reactive.Bindings.Extensions;
187
+ public string Name { get => _Name; set => Set(ref _Name, value); }
104
188
 
105
- namespace Questions292544
106
- {
107
- public class TodoViewModel
189
+ bool _IsExpanded;
108
- {
109
- public ReadOnlyReactiveCollection<TaskViewModel> TaskList { get; }
190
+ public bool IsExpanded { get => _IsExpanded; set => Set(ref _IsExpanded, value); }
110
- public ReactiveProperty<TaskViewModel> Selected { get; } = new ReactiveProperty<TaskViewModel>();
111
- public ReactiveCommand AddCommand { get; }
112
- public ReactiveCommand SaveCommand { get; }
113
191
 
114
- private readonly TodoModel todoModel;
192
+ bool _IsSelected;
193
+ public bool IsSelected { get => _IsSelected; set => Set(ref _IsSelected, value); }
115
194
 
195
+ bool? _IsChecked = false;
116
- public TodoViewModel(TodoModel todoModel)
196
+ public bool? IsChecked // ThreeState
117
- {
197
+ {
198
+ get => _IsChecked;
199
+ set
200
+ {
201
+ if(_IsChecked == value) return;
202
+ if(reentrancyCheck) return; // 再入防止
118
- this.todoModel = todoModel;
203
+ reentrancyCheck = true;
119
- TaskList = todoModel.TaskList.ToReadOnlyReactiveCollection(x => new TaskViewModel(x));
204
+ Set(ref _IsChecked, value);
120
- TaskList.ObserveAddChanged().Subscribe(x => Selected.Value = x);
205
+ UpdateCheckState(); // チェックが変わったら上下も更新
206
+ reentrancyCheck = false;
207
+ }
208
+ }
121
209
 
122
- AddCommand = new ReactiveCommand().WithSubscribe(() => todoModel.TaskList.Add(new TaskModel()));
210
+ public ObservableCollection<TreeNode> Children { get; } = new ObservableCollection<TreeNode>();
123
- SaveCommand = new ReactiveCommand().WithSubscribe(() => todoModel.Save());
124
- }
125
- }
126
- }
127
- ```
128
- TodoModel.cs
129
- ```C#
130
- using System.Collections.ObjectModel;
131
- using System.Runtime.Serialization;
132
- using System.Text;
133
- using System.Xml;
134
211
 
135
- namespace Questions292544
136
- {
137
- [DataContract(Namespace = "")]
138
- public class TodoModel
139
- {
140
- [DataMember] public ObservableCollection<TaskModel> TaskList { get; private set; } = new ObservableCollection<TaskModel>();
212
+ public TreeNode Parent { get; private set; } // 追加・削除の簡便さのため親ノードが欲しい
141
213
 
142
- private string path;
214
+ bool reentrancyCheck;
143
215
 
144
- public TodoModel()
145
- {
146
- TaskList.Add(new TaskModel { Title = "aaa", });
216
+ public TreeNode(string name) => Name = name;
147
- TaskList.Add(new TaskModel { Title = "bbbbbb", });
148
- TaskList.Add(new TaskModel { Title = "ccccccccc", });
149
- }
150
217
 
151
- public static TodoModel Load(string path)
218
+ public void Add(TreeNode child)
152
- {
219
+ {
220
+ child.Parent = this;
221
+ Children.Add(child);
222
+ IsExpanded = true;
223
+ child.UpdateCheckState();
153
- try
224
+ }
225
+ public void Remove(TreeNode child)
154
- {
226
+ {
155
- var serializer = new DataContractSerializer(typeof(TodoModel));
156
- using(var xr = XmlReader.Create(path))
227
+ Children.Remove(child);
157
- {
158
- var m = (TodoModel)serializer.ReadObject(xr);
159
- m.path = path;
228
+ child.Parent = null;
160
- return m;
229
+ if(0 < Children.Count) IsChecked = DetermineCheckState();
161
- }
230
+ }
162
- }
163
- catch { return new TodoModel { path = path, }; }
164
- }
165
231
 
232
+ //チェック状態反映
233
+ // https://docs.telerik.com/devtools/wpf/controls/radtreeview/how-to/howto-tri-state-mvvm
166
- public void Save()
234
+ void UpdateCheckState()
167
- {
235
+ {
168
- var serializer = new DataContractSerializer(typeof(TodoModel));
236
+ if(0 < Children.Count) UpdateChildrenCheckState();
237
+ if(Parent != null) Parent.IsChecked = Parent.DetermineCheckState();
238
+ }
169
- var settings = new XmlWriterSettings
239
+ void UpdateChildrenCheckState()
170
- {
240
+ {
171
- Encoding = new UTF8Encoding(false),
241
+ foreach(var c in Children)
172
- Indent = true,
173
- };
174
- using(var xw = XmlWriter.Create(path, settings))
175
- {
242
+ {
176
- serializer.WriteObject(xw, this);
243
+ if(IsChecked != null) c.IsChecked = IsChecked;
177
- }
244
+ }
178
- }
245
+ }
246
+ bool? DetermineCheckState()
179
- }
247
+ {
180
- }
248
+ var checkCount = Children.Count(x => x.IsChecked == true);
181
- ```
249
+ if(checkCount == Children.Count) return true;
182
250
 
251
+ var uncheckCount = Children.Count(x => x.IsChecked == false);
183
- ---
252
+ if(uncheckCount == Children.Count) return false;
184
253
 
185
- TaskView.xaml
186
- ```xaml
187
- <UserControl
188
- x:Class="Questions292544.TaskView"
189
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
190
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
191
- MinWidth="100">
192
- <Grid>
254
+ return null;
193
- <Grid.ColumnDefinitions>
194
- <ColumnDefinition Width="Auto" />
195
- <ColumnDefinition Width="Auto" />
196
- <ColumnDefinition />
197
- </Grid.ColumnDefinitions>
198
- <CheckBox VerticalAlignment="Center" IsChecked="{Binding Done.Value}" />
199
- <TextBlock
200
- Grid.Column="1"
201
- Margin="10,0"
202
- xml:lang="ja"
203
- Text="{Binding Created.Value, StringFormat=d}" />
204
- <TextBlock Grid.Column="2" Text="{Binding Title.Value}" />
205
- </Grid>
206
- </UserControl>
207
- ```
255
+ }
208
- TaskViewModel.cs
209
- ```C#
210
- using System;
211
- using Reactive.Bindings;
212
256
 
213
- namespace Questions292544
214
- {
215
- public class TaskViewModel
257
+ // 子孫を全列挙(Children列挙でないので注意)
216
- {
258
+ IEnumerable<TreeNode> Descendants(TreeNode node) => node.Children.Concat(node.Children.SelectMany(Descendants));
217
- public ReactiveProperty<bool> Done { get; }
259
+ public IEnumerator<TreeNode> GetEnumerator() => Descendants(this).GetEnumerator();
218
- public ReactiveProperty<DateTime> Created { get; }
260
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
219
- public ReactiveProperty<string> Title { get; }
261
+ }
220
262
 
221
- public TaskViewModel(TaskModel task)
263
+ class Observable : INotifyPropertyChanged
222
- {
264
+ {
223
- Done = ReactiveProperty.FromObject(task, x => x.Done);
224
- Created = ReactiveProperty.FromObject(task, x => x.Created);
265
+ public event PropertyChangedEventHandler PropertyChanged;
225
- Title = ReactiveProperty.FromObject(task, x => x.Title);
266
+ protected void Set<T>(ref T storage, T value, [CallerMemberName] string name = null)
267
+ {
268
+ if(Equals(storage, value)) return;
269
+ storage = value;
270
+ OnPropertyChanged(name);
226
- }
271
+ }
272
+ protected void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
227
- }
273
+ }
274
+ class RelayCommand : ICommand
275
+ {
276
+ readonly Action exec;
277
+ readonly Func<bool> can;
278
+ public event EventHandler CanExecuteChanged;
279
+ public RelayCommand(Action e) : this(e, null) { }
280
+ public RelayCommand(Action e, Func<bool> c)
281
+ {
282
+ exec = e ?? throw new ArgumentNullException(nameof(e));
283
+ can = c;
284
+ }
285
+ public bool CanExecute(object _) => can == null || can();
286
+ public void Execute(object _) => exec();
287
+ public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
288
+ }
289
+ class RelayCommand<T> : ICommand
290
+ {
291
+ readonly Action<T> exec;
292
+ readonly Func<T, bool> can;
293
+ public event EventHandler CanExecuteChanged;
294
+ public RelayCommand(Action<T> e) : this(e, null) { }
295
+ public RelayCommand(Action<T> e, Func<T, bool> c)
296
+ {
297
+ exec = e ?? throw new ArgumentNullException(nameof(e));
298
+ can = c;
299
+ }
300
+ public bool CanExecute(object p) => can == null || can((T)p);
301
+ public void Execute(object p) => exec((T)p);
302
+ public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
303
+ }
228
304
  }
229
305
  ```
230
- TaskModel.cs
306
+ .xaml.csは初期状態
231
- ```C#
232
- using System;
233
- using System.Runtime.Serialization;
234
307
 
235
- namespace Questions292544
236
- {
308
+ ---
237
- [DataContract(Namespace = "")]
238
- public class TaskModel
239
- {
240
- [DataMember] public bool Done { get; set; }
241
- [DataMember] public DateTime Created { get; private set; }
242
- [DataMember] public string Title { get; set; }
243
309
 
244
- public TaskModel() => Created = DateTime.Now;
310
+ `TreeNode`は厳密には`ViewModel`になるかもしれませんが、ライブラリなしではめんどくさいのでVM・M兼用です^^;
245
- }
246
- }
247
- ```
248
-
249
- xaml.cs(コードビハインド)はすべて初期状態
250
- コード量を抑えつつ、細切れにするのは難しいですね^^;