回答編集履歴

2

見直しキャンペーン中

2023/07/23 06:17

投稿

TN8001
TN8001

スコア9862

test CHANGED
@@ -1,619 +1,308 @@
1
1
  過去の回答の使いまわしなので、例としてはちょっと重めですが
2
-
3
- ```xaml
2
+ ```xml
4
-
5
3
  <!-- 直置きパターン -->
6
-
7
4
  <local:TreeUserControl DataContext="{Binding Tree}" />
8
-
9
5
  <!-- DataTemplateパターン -->
10
-
11
6
  <ContentPresenter Grid.Column="1" Content="{Binding Tree}" />
12
-
13
7
  ```
14
-
15
8
  この2つの違いと、(結果的には何も変わらないのですが^^; 初めて見たときは「はぁ~なるほど!」と思いました)
16
-
17
9
  `TreeViewModel`・`TreeUserControl`の関係を見てください。
18
10
 
19
-
20
-
21
11
  `TreeUserControl`が`TreeView`だけなのは、ちょっともったいない?かもしれません。
22
-
23
12
  ボタン類も一緒に入れると、責務がきれいに分かれたようになると思います。
24
13
 
25
-
26
-
27
14
  `MainViewModel`が`TreeViewModel`を保持しているので、`MainViewModel`からツリーの状態はいつでも見れます。
28
15
 
29
-
30
-
31
16
  `DependencyProperty`は特に出番はありませんでした。
32
-
33
17
  左右は同じ`TreeViewModel`を見ているので、自動的に連動します。
34
-
35
18
  これでは意味がないですが、チェックされてるものだけ表示する(`TreeView`だと大変すぎるが^^; `ListBox`なんかは簡単)等、差をつけるときに`DependencyProperty`があれば便利です。
36
19
 
37
-
38
-
39
- MainWindow.xaml
20
+ ```xml:MainWindow.xaml
40
-
41
- ```xaml
42
-
43
21
  <Window
44
-
45
22
  x:Class="Questions292544.MainWindow"
46
-
47
23
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
48
-
49
24
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
50
-
51
25
  xmlns:local="clr-namespace:Questions292544"
52
-
53
26
  Width="800"
54
-
55
27
  Height="450">
56
-
57
28
  <Window.DataContext>
58
-
59
29
  <local:MainViewModel />
60
-
61
30
  </Window.DataContext>
62
-
63
31
  <Window.Resources>
64
-
65
32
  <DataTemplate DataType="{x:Type local:TreeViewModel}">
66
-
67
33
  <local:TreeUserControl />
68
-
69
34
  </DataTemplate>
70
-
71
35
  <Style TargetType="Button">
72
-
73
36
  <Setter Property="Margin" Value="5" />
74
-
75
37
  </Style>
76
-
77
38
  </Window.Resources>
78
-
79
39
  <DockPanel>
80
-
81
40
  <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
82
-
83
41
  <Button Command="{Binding Tree.AllCheckCmd}" Content="全チェック" />
84
-
85
42
  <Button Command="{Binding Tree.AllUncheckCmd}" Content="全アンチェック" />
86
-
87
43
  <Button Command="{Binding Tree.AllExpandCmd}" Content="全展開" />
88
-
89
44
  <Button Command="{Binding Tree.AllContractCmd}" Content="全畳む" />
90
-
91
45
  <Button Command="{Binding Tree.AddNodeCmd}" Content="ノード追加" />
92
-
93
46
  <Button Command="{Binding PrintCheckedNodesCmd}" Content="チェックノード表示(Debug)" />
94
-
95
47
  </StackPanel>
96
-
97
48
  <Grid>
98
-
99
49
  <Grid.ColumnDefinitions>
100
-
101
50
  <ColumnDefinition />
102
-
103
51
  <ColumnDefinition />
104
-
105
52
  </Grid.ColumnDefinitions>
106
-
107
53
  <!-- 直置きパターン -->
108
-
109
54
  <local:TreeUserControl DataContext="{Binding Tree}" />
110
-
111
55
  <!-- DataTemplateパターン -->
112
-
113
56
  <ContentPresenter Grid.Column="1" Content="{Binding Tree}" />
114
-
115
57
  </Grid>
116
-
117
58
  </DockPanel>
118
-
119
59
  </Window>
120
-
121
60
  ```
122
61
 
123
-
124
-
125
- TreeUserControl.xaml
62
+ ```xml:TreeUserControl.xaml
126
-
127
- ```xaml
128
-
129
63
  <UserControl
130
-
131
64
  x:Class="Questions292544.TreeUserControl"
132
-
133
65
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
134
-
135
66
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
136
-
137
67
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
138
-
139
68
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
140
-
141
69
  d:DesignHeight="450"
142
-
143
70
  d:DesignWidth="800"
144
-
145
71
  mc:Ignorable="d">
146
-
147
72
  <TreeView ItemsSource="{Binding Root.Children}">
148
-
149
73
  <TreeView.ItemTemplate>
150
-
151
74
  <HierarchicalDataTemplate ItemsSource="{Binding Children}">
152
-
153
75
  <StackPanel Orientation="Horizontal">
154
-
155
76
  <CheckBox
156
-
157
77
  Margin="5"
158
-
159
78
  VerticalContentAlignment="Center"
160
-
161
79
  IsChecked="{Binding IsChecked}" />
162
-
163
80
  <TextBlock Margin="5" Text="{Binding Name}" />
164
-
165
81
  <Button
166
-
167
82
  Margin="5"
168
-
169
83
  Command="{Binding DataContext.DeleteNodeCmd, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeView}}}"
170
-
171
84
  CommandParameter="{Binding}"
172
-
173
85
  Content="削除" />
174
-
175
86
  </StackPanel>
176
-
177
87
  </HierarchicalDataTemplate>
178
-
179
88
  </TreeView.ItemTemplate>
180
-
181
89
  <TreeView.ItemContainerStyle>
182
-
183
90
  <Style TargetType="{x:Type TreeViewItem}">
184
-
185
91
  <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
186
-
187
92
  <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
188
-
189
93
  </Style>
190
-
191
94
  </TreeView.ItemContainerStyle>
192
-
193
95
  </TreeView>
194
-
195
96
  </UserControl>
196
-
197
97
  ```
198
98
 
199
-
200
-
201
99
  他コード
202
-
203
- ```C#
100
+ ```cs
204
-
205
101
  using System;
206
-
207
102
  using System.Collections;
208
-
209
103
  using System.Collections.Generic;
210
-
211
104
  using System.Collections.ObjectModel;
212
-
213
105
  using System.ComponentModel;
214
-
215
106
  using System.Diagnostics;
216
-
217
107
  using System.Linq;
218
-
219
108
  using System.Runtime.CompilerServices;
220
-
221
109
  using System.Windows.Input;
222
110
 
223
-
224
-
225
111
  namespace Questions292544
226
-
227
112
  {
228
-
229
113
  class MainViewModel : Observable
230
-
231
- {
114
+ {
232
-
233
115
  public TreeViewModel Tree { get; } = new TreeViewModel();
234
-
235
116
  public RelayCommand PrintCheckedNodesCmd { get; }
236
-
237
117
  public MainViewModel()
238
-
239
- {
118
+ {
240
-
241
119
  PrintCheckedNodesCmd = new RelayCommand(() =>
242
-
243
120
  {
244
-
245
121
  Debug.WriteLine("IsChecked"); // チェックを入れた最上位の情報表示
246
122
 
247
-
248
-
249
123
  foreach(var c in Tree.Root.Children) Dfs(c);
250
124
 
251
-
252
-
253
125
  void Dfs(TreeNode node) // 深さ優先探索
254
-
255
126
  {
256
-
257
127
  if(node.IsChecked == true)
258
-
259
128
  {
260
-
261
129
  Debug.WriteLine(node.Name);
262
-
263
130
  return;
264
-
265
131
  }
266
-
267
132
  foreach(var c in node.Children) Dfs(c);
268
-
269
133
  }
270
-
271
134
  });
272
-
273
- }
135
+ }
274
-
275
- }
136
+ }
276
-
277
-
278
137
 
279
138
  class TreeViewModel : Observable
280
-
281
- {
139
+ {
282
-
283
140
  public TreeNode Root { get; }
284
141
 
285
-
286
-
287
142
  public RelayCommand AllCheckCmd { get; }
288
-
289
143
  public RelayCommand AllUncheckCmd { get; }
290
-
291
144
  public RelayCommand AllExpandCmd { get; }
292
-
293
145
  public RelayCommand AllContractCmd { get; }
294
-
295
146
  public RelayCommand<TreeNode> DeleteNodeCmd { get; }
296
-
297
147
  public RelayCommand AddNodeCmd { get; }
298
148
 
299
-
300
-
301
149
  public TreeViewModel()
302
-
303
- {
150
+ {
304
-
305
151
  Root = new TreeNode("Root") {
306
-
307
152
  new TreeNode("Node1") {
308
-
309
153
  new TreeNode("Node1-1"),
310
-
311
154
  new TreeNode("Node1-2") {
312
-
313
155
  new TreeNode("Node1-2-1"),
314
-
315
156
  new TreeNode("Node1-2-2"),
316
-
317
157
  },
318
-
319
158
  },
320
-
321
159
  new TreeNode("Node2") {
322
-
323
160
  new TreeNode("Node2-1") {
324
-
325
161
  new TreeNode("Node2-1-1"),
326
-
327
162
  new TreeNode("Node2-1-2"),
328
-
329
163
  },
330
-
331
164
  },
332
-
333
165
  };
334
166
 
335
-
336
-
337
167
  AllCheckCmd = new RelayCommand(() => { foreach(var c in Root) c.IsChecked = true; });
338
-
339
168
  AllUncheckCmd = new RelayCommand(() => { foreach(var c in Root) c.IsChecked = false; });
340
-
341
169
  AllExpandCmd = new RelayCommand(() => { foreach(var c in Root) c.IsExpanded = true; });
342
-
343
170
  AllContractCmd = new RelayCommand(() => { foreach(var c in Root) c.IsExpanded = false; });
344
-
345
171
  DeleteNodeCmd = new RelayCommand<TreeNode>((c) => c.Parent.Remove(c));
346
-
347
172
  AddNodeCmd = new RelayCommand(() =>
348
-
349
173
  {
350
-
351
174
  // 選択の子に追加
352
-
353
175
  if(Root.FirstOrDefault(x => x.IsSelected) is TreeNode selectedItem)
354
-
355
176
  selectedItem.Add(new TreeNode("NewNode"));
356
-
357
177
  else Root.Add(new TreeNode("NewNode"));
358
-
359
178
  });
360
-
361
- }
179
+ }
362
-
363
- }
180
+ }
364
-
365
-
366
181
 
367
182
  class TreeNode : Observable, IEnumerable<TreeNode>
368
-
369
- {
183
+ {
370
-
371
184
  string _Name;
372
-
373
185
  public string Name { get => _Name; set => Set(ref _Name, value); }
374
186
 
375
-
376
-
377
187
  bool _IsExpanded;
378
-
379
188
  public bool IsExpanded { get => _IsExpanded; set => Set(ref _IsExpanded, value); }
380
189
 
381
-
382
-
383
190
  bool _IsSelected;
384
-
385
191
  public bool IsSelected { get => _IsSelected; set => Set(ref _IsSelected, value); }
386
192
 
387
-
388
-
389
193
  bool? _IsChecked = false;
390
-
391
194
  public bool? IsChecked // ThreeState
392
-
393
- {
195
+ {
394
-
395
196
  get => _IsChecked;
396
-
397
197
  set
398
-
399
198
  {
400
-
401
199
  if(_IsChecked == value) return;
402
-
403
200
  if(reentrancyCheck) return; // 再入防止
404
-
405
201
  reentrancyCheck = true;
406
-
407
202
  Set(ref _IsChecked, value);
408
-
409
203
  UpdateCheckState(); // チェックが変わったら上下も更新
410
-
411
204
  reentrancyCheck = false;
412
-
413
205
  }
414
-
415
- }
206
+ }
416
-
417
-
418
207
 
419
208
  public ObservableCollection<TreeNode> Children { get; } = new ObservableCollection<TreeNode>();
420
209
 
421
-
422
-
423
210
  public TreeNode Parent { get; private set; } // 追加・削除の簡便さのため親ノードが欲しい
424
211
 
425
-
426
-
427
212
  bool reentrancyCheck;
428
213
 
429
-
430
-
431
214
  public TreeNode(string name) => Name = name;
432
215
 
433
-
434
-
435
216
  public void Add(TreeNode child)
436
-
437
- {
217
+ {
438
-
439
218
  child.Parent = this;
440
-
441
219
  Children.Add(child);
442
-
443
220
  IsExpanded = true;
444
-
445
221
  child.UpdateCheckState();
446
-
447
- }
222
+ }
448
-
449
223
  public void Remove(TreeNode child)
450
-
451
- {
224
+ {
452
-
453
225
  Children.Remove(child);
454
-
455
226
  child.Parent = null;
456
-
457
227
  if(0 < Children.Count) IsChecked = DetermineCheckState();
458
-
459
- }
228
+ }
460
-
461
-
462
229
 
463
230
  //チェック状態反映
464
-
465
231
  // https://docs.telerik.com/devtools/wpf/controls/radtreeview/how-to/howto-tri-state-mvvm
466
-
467
232
  void UpdateCheckState()
468
-
469
- {
233
+ {
470
-
471
234
  if(0 < Children.Count) UpdateChildrenCheckState();
472
-
473
235
  if(Parent != null) Parent.IsChecked = Parent.DetermineCheckState();
474
-
475
- }
236
+ }
476
-
477
237
  void UpdateChildrenCheckState()
478
-
479
- {
238
+ {
480
-
481
239
  foreach(var c in Children)
482
-
483
240
  {
484
-
485
241
  if(IsChecked != null) c.IsChecked = IsChecked;
486
-
487
242
  }
488
-
489
- }
243
+ }
490
-
491
244
  bool? DetermineCheckState()
492
-
493
- {
245
+ {
494
-
495
246
  var checkCount = Children.Count(x => x.IsChecked == true);
496
-
497
247
  if(checkCount == Children.Count) return true;
498
248
 
499
-
500
-
501
249
  var uncheckCount = Children.Count(x => x.IsChecked == false);
502
-
503
250
  if(uncheckCount == Children.Count) return false;
504
251
 
505
-
506
-
507
252
  return null;
508
-
509
- }
253
+ }
510
-
511
-
512
254
 
513
255
  // 子孫を全列挙(Children列挙でないので注意)
514
-
515
256
  IEnumerable<TreeNode> Descendants(TreeNode node) => node.Children.Concat(node.Children.SelectMany(Descendants));
516
-
517
257
  public IEnumerator<TreeNode> GetEnumerator() => Descendants(this).GetEnumerator();
518
-
519
258
  IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
520
-
521
- }
259
+ }
522
-
523
-
524
260
 
525
261
  class Observable : INotifyPropertyChanged
526
-
527
- {
262
+ {
528
-
529
263
  public event PropertyChangedEventHandler PropertyChanged;
530
-
531
264
  protected void Set<T>(ref T storage, T value, [CallerMemberName] string name = null)
532
-
533
- {
265
+ {
534
-
535
266
  if(Equals(storage, value)) return;
536
-
537
267
  storage = value;
538
-
539
268
  OnPropertyChanged(name);
540
-
541
- }
269
+ }
542
-
543
270
  protected void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
544
-
545
- }
271
+ }
546
-
547
272
  class RelayCommand : ICommand
548
-
549
- {
273
+ {
550
-
551
274
  readonly Action exec;
552
-
553
275
  readonly Func<bool> can;
554
-
555
276
  public event EventHandler CanExecuteChanged;
556
-
557
277
  public RelayCommand(Action e) : this(e, null) { }
558
-
559
278
  public RelayCommand(Action e, Func<bool> c)
560
-
561
- {
279
+ {
562
-
563
280
  exec = e ?? throw new ArgumentNullException(nameof(e));
564
-
565
281
  can = c;
566
-
567
- }
282
+ }
568
-
569
283
  public bool CanExecute(object _) => can == null || can();
570
-
571
284
  public void Execute(object _) => exec();
572
-
573
285
  public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
574
-
575
- }
286
+ }
576
-
577
287
  class RelayCommand<T> : ICommand
578
-
579
- {
288
+ {
580
-
581
289
  readonly Action<T> exec;
582
-
583
290
  readonly Func<T, bool> can;
584
-
585
291
  public event EventHandler CanExecuteChanged;
586
-
587
292
  public RelayCommand(Action<T> e) : this(e, null) { }
588
-
589
293
  public RelayCommand(Action<T> e, Func<T, bool> c)
590
-
591
- {
294
+ {
592
-
593
295
  exec = e ?? throw new ArgumentNullException(nameof(e));
594
-
595
296
  can = c;
596
-
597
- }
297
+ }
598
-
599
298
  public bool CanExecute(object p) => can == null || can((T)p);
600
-
601
299
  public void Execute(object p) => exec((T)p);
602
-
603
300
  public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
604
-
605
- }
301
+ }
606
-
607
302
  }
608
-
609
303
  ```
610
-
611
304
  .xaml.csは初期状態
612
305
 
613
-
614
-
615
306
  ---
616
307
 
617
-
618
-
619
308
  `TreeNode`は厳密には`ViewModel`になるかもしれませんが、ライブラリなしではめんどくさいのでVM・M兼用です^^;

1

全面書き直し

2020/09/20 02:14

投稿

TN8001
TN8001

スコア9862

test CHANGED
@@ -1,16 +1,38 @@
1
+ 過去の回答の使いまわしなので、例としてはちょっと重めですが
2
+
3
+ ```xaml
4
+
5
+ <!-- 直置きパターン -->
6
+
7
+ <local:TreeUserControl DataContext="{Binding Tree}" />
8
+
9
+ <!-- DataTemplateパターン -->
10
+
11
+ <ContentPresenter Grid.Column="1" Content="{Binding Tree}" />
12
+
13
+ ```
14
+
15
+ この2つの違いと、(結果的には何も変わらないのですが^^; 初めて見たときは「はぁ~なるほど!」と思いました)
16
+
17
+ `TreeViewModel`・`TreeUserControl`の関係を見てください。
18
+
19
+
20
+
1
- > UserControlがPrism的部分ビュー
21
+ `TreeUserControl``TreeView`だけのは、ちょっともったいない?かもしれません。
2
-
3
-
4
-
22
+
5
- の話になます。ToDoアプリっぽいものを例題にしました。
23
+ ボタン類も一緒入れると、責務がきれいに分かれたようにると思います。
6
-
24
+
25
+
26
+
7
- `MainViewModel``TodoViewModel``TaskViewModel`の関係が、かに`View`に展開さるかを確認してください
27
+ `MainViewModel``TreeViewModel`を保持しているので、`MainViewModel`からツリー状態はつでも見ます
8
-
28
+
29
+
30
+
9
- `Model`部分手抜きなので重要ではありません^^;
31
+ `DependencyProperty`は特に出番はありませんでした。
32
+
10
-
33
+ 左右は同じ`TreeViewModel`を見ているので、自動的に連動します。
11
-
12
-
34
+
13
- [NuGet Gallery | ReactiveProperty.WPF 7.3.0](https://www.nuget.org/packages/ReactiveProperty.WPF)必要になります。
35
+ これでは意味がないですが、チェックされてるものだけ表示する(`TreeView`だと大変すぎるが^^; `ListBox`なんかは簡単)等、差をつけるときに`DependencyProperty`あれば便利です。
14
36
 
15
37
 
16
38
 
@@ -26,26 +48,12 @@
26
48
 
27
49
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
28
50
 
29
- xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
30
-
31
51
  xmlns:local="clr-namespace:Questions292544"
32
52
 
33
- xmlns:rp="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
34
-
35
53
  Width="800"
36
54
 
37
55
  Height="450">
38
56
 
39
- <i:Interaction.Triggers>
40
-
41
- <i:EventTrigger EventName="Closing">
42
-
43
- <rp:EventToReactiveCommand Command="{Binding TodoViewModel.SaveCommand}" />
44
-
45
- </i:EventTrigger>
46
-
47
- </i:Interaction.Triggers>
48
-
49
57
  <Window.DataContext>
50
58
 
51
59
  <local:MainViewModel />
@@ -54,25 +62,57 @@
54
62
 
55
63
  <Window.Resources>
56
64
 
57
- <DataTemplate DataType="{x:Type local:TodoViewModel}">
65
+ <DataTemplate DataType="{x:Type local:TreeViewModel}">
58
-
66
+
59
- <local:TodoView />
67
+ <local:TreeUserControl />
60
68
 
61
69
  </DataTemplate>
62
70
 
71
+ <Style TargetType="Button">
72
+
73
+ <Setter Property="Margin" Value="5" />
74
+
75
+ </Style>
76
+
63
77
  </Window.Resources>
64
78
 
65
79
  <DockPanel>
66
80
 
81
+ <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
82
+
83
+ <Button Command="{Binding Tree.AllCheckCmd}" Content="全チェック" />
84
+
85
+ <Button Command="{Binding Tree.AllUncheckCmd}" Content="全アンチェック" />
86
+
87
+ <Button Command="{Binding Tree.AllExpandCmd}" Content="全展開" />
88
+
89
+ <Button Command="{Binding Tree.AllContractCmd}" Content="全畳む" />
90
+
91
+ <Button Command="{Binding Tree.AddNodeCmd}" Content="ノード追加" />
92
+
93
+ <Button Command="{Binding PrintCheckedNodesCmd}" Content="チェックノード表示(Debug)" />
94
+
95
+ </StackPanel>
96
+
67
- <TextBlock
97
+ <Grid>
68
-
98
+
69
- HorizontalAlignment="Center"
99
+ <Grid.ColumnDefinitions>
70
-
71
- DockPanel.Dock="Top"
100
+
72
-
73
- Text="TodoList" />
101
+ <ColumnDefinition />
102
+
74
-
103
+ <ColumnDefinition />
104
+
105
+ </Grid.ColumnDefinitions>
106
+
107
+ <!-- 直置きパターン -->
108
+
75
- <ContentPresenter Content="{Binding TodoViewModel}" />
109
+ <local:TreeUserControl DataContext="{Binding Tree}" />
110
+
111
+ <!-- DataTemplateパターン -->
112
+
113
+ <ContentPresenter Grid.Column="1" Content="{Binding Tree}" />
114
+
115
+ </Grid>
76
116
 
77
117
  </DockPanel>
78
118
 
@@ -80,420 +120,500 @@
80
120
 
81
121
  ```
82
122
 
123
+
124
+
83
- MainViewModel.cs
125
+ TreeUserControl.xaml
126
+
127
+ ```xaml
128
+
129
+ <UserControl
130
+
131
+ x:Class="Questions292544.TreeUserControl"
132
+
133
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
134
+
135
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
136
+
137
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
138
+
139
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
140
+
141
+ d:DesignHeight="450"
142
+
143
+ d:DesignWidth="800"
144
+
145
+ mc:Ignorable="d">
146
+
147
+ <TreeView ItemsSource="{Binding Root.Children}">
148
+
149
+ <TreeView.ItemTemplate>
150
+
151
+ <HierarchicalDataTemplate ItemsSource="{Binding Children}">
152
+
153
+ <StackPanel Orientation="Horizontal">
154
+
155
+ <CheckBox
156
+
157
+ Margin="5"
158
+
159
+ VerticalContentAlignment="Center"
160
+
161
+ IsChecked="{Binding IsChecked}" />
162
+
163
+ <TextBlock Margin="5" Text="{Binding Name}" />
164
+
165
+ <Button
166
+
167
+ Margin="5"
168
+
169
+ Command="{Binding DataContext.DeleteNodeCmd, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeView}}}"
170
+
171
+ CommandParameter="{Binding}"
172
+
173
+ Content="削除" />
174
+
175
+ </StackPanel>
176
+
177
+ </HierarchicalDataTemplate>
178
+
179
+ </TreeView.ItemTemplate>
180
+
181
+ <TreeView.ItemContainerStyle>
182
+
183
+ <Style TargetType="{x:Type TreeViewItem}">
184
+
185
+ <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
186
+
187
+ <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
188
+
189
+ </Style>
190
+
191
+ </TreeView.ItemContainerStyle>
192
+
193
+ </TreeView>
194
+
195
+ </UserControl>
196
+
197
+ ```
198
+
199
+
200
+
201
+ 他コード
84
202
 
85
203
  ```C#
86
204
 
205
+ using System;
206
+
207
+ using System.Collections;
208
+
209
+ using System.Collections.Generic;
210
+
211
+ using System.Collections.ObjectModel;
212
+
87
213
  using System.ComponentModel;
88
214
 
215
+ using System.Diagnostics;
216
+
217
+ using System.Linq;
218
+
219
+ using System.Runtime.CompilerServices;
220
+
221
+ using System.Windows.Input;
222
+
89
223
 
90
224
 
91
225
  namespace Questions292544
92
226
 
93
227
  {
94
228
 
95
- internal class MainViewModel : INotifyPropertyChanged
96
-
97
- {
98
-
99
- public TodoViewModel TodoViewModel { get; }
100
-
101
-
102
-
103
- public MainViewModel() => TodoViewModel = new TodoViewModel(TodoModel.Load("test.xml"));
104
-
105
-
106
-
107
- public event PropertyChangedEventHandler PropertyChanged;
108
-
109
- }
229
+ class MainViewModel : Observable
230
+
231
+ {
232
+
233
+ public TreeViewModel Tree { get; } = new TreeViewModel();
234
+
235
+ public RelayCommand PrintCheckedNodesCmd { get; }
236
+
237
+ public MainViewModel()
238
+
239
+ {
240
+
241
+ PrintCheckedNodesCmd = new RelayCommand(() =>
242
+
243
+ {
244
+
245
+ Debug.WriteLine("IsChecked"); // チェックを入れた最上位の情報表示
246
+
247
+
248
+
249
+ foreach(var c in Tree.Root.Children) Dfs(c);
250
+
251
+
252
+
253
+ void Dfs(TreeNode node) // 深さ優先探索
254
+
255
+ {
256
+
257
+ if(node.IsChecked == true)
258
+
259
+ {
260
+
261
+ Debug.WriteLine(node.Name);
262
+
263
+ return;
264
+
265
+ }
266
+
267
+ foreach(var c in node.Children) Dfs(c);
268
+
269
+ }
270
+
271
+ });
272
+
273
+ }
274
+
275
+ }
276
+
277
+
278
+
279
+ class TreeViewModel : Observable
280
+
281
+ {
282
+
283
+ public TreeNode Root { get; }
284
+
285
+
286
+
287
+ public RelayCommand AllCheckCmd { get; }
288
+
289
+ public RelayCommand AllUncheckCmd { get; }
290
+
291
+ public RelayCommand AllExpandCmd { get; }
292
+
293
+ public RelayCommand AllContractCmd { get; }
294
+
295
+ public RelayCommand<TreeNode> DeleteNodeCmd { get; }
296
+
297
+ public RelayCommand AddNodeCmd { get; }
298
+
299
+
300
+
301
+ public TreeViewModel()
302
+
303
+ {
304
+
305
+ Root = new TreeNode("Root") {
306
+
307
+ new TreeNode("Node1") {
308
+
309
+ new TreeNode("Node1-1"),
310
+
311
+ new TreeNode("Node1-2") {
312
+
313
+ new TreeNode("Node1-2-1"),
314
+
315
+ new TreeNode("Node1-2-2"),
316
+
317
+ },
318
+
319
+ },
320
+
321
+ new TreeNode("Node2") {
322
+
323
+ new TreeNode("Node2-1") {
324
+
325
+ new TreeNode("Node2-1-1"),
326
+
327
+ new TreeNode("Node2-1-2"),
328
+
329
+ },
330
+
331
+ },
332
+
333
+ };
334
+
335
+
336
+
337
+ AllCheckCmd = new RelayCommand(() => { foreach(var c in Root) c.IsChecked = true; });
338
+
339
+ AllUncheckCmd = new RelayCommand(() => { foreach(var c in Root) c.IsChecked = false; });
340
+
341
+ AllExpandCmd = new RelayCommand(() => { foreach(var c in Root) c.IsExpanded = true; });
342
+
343
+ AllContractCmd = new RelayCommand(() => { foreach(var c in Root) c.IsExpanded = false; });
344
+
345
+ DeleteNodeCmd = new RelayCommand<TreeNode>((c) => c.Parent.Remove(c));
346
+
347
+ AddNodeCmd = new RelayCommand(() =>
348
+
349
+ {
350
+
351
+ // 選択の子に追加
352
+
353
+ if(Root.FirstOrDefault(x => x.IsSelected) is TreeNode selectedItem)
354
+
355
+ selectedItem.Add(new TreeNode("NewNode"));
356
+
357
+ else Root.Add(new TreeNode("NewNode"));
358
+
359
+ });
360
+
361
+ }
362
+
363
+ }
364
+
365
+
366
+
367
+ class TreeNode : Observable, IEnumerable<TreeNode>
368
+
369
+ {
370
+
371
+ string _Name;
372
+
373
+ public string Name { get => _Name; set => Set(ref _Name, value); }
374
+
375
+
376
+
377
+ bool _IsExpanded;
378
+
379
+ public bool IsExpanded { get => _IsExpanded; set => Set(ref _IsExpanded, value); }
380
+
381
+
382
+
383
+ bool _IsSelected;
384
+
385
+ public bool IsSelected { get => _IsSelected; set => Set(ref _IsSelected, value); }
386
+
387
+
388
+
389
+ bool? _IsChecked = false;
390
+
391
+ public bool? IsChecked // ThreeState
392
+
393
+ {
394
+
395
+ get => _IsChecked;
396
+
397
+ set
398
+
399
+ {
400
+
401
+ if(_IsChecked == value) return;
402
+
403
+ if(reentrancyCheck) return; // 再入防止
404
+
405
+ reentrancyCheck = true;
406
+
407
+ Set(ref _IsChecked, value);
408
+
409
+ UpdateCheckState(); // チェックが変わったら上下も更新
410
+
411
+ reentrancyCheck = false;
412
+
413
+ }
414
+
415
+ }
416
+
417
+
418
+
419
+ public ObservableCollection<TreeNode> Children { get; } = new ObservableCollection<TreeNode>();
420
+
421
+
422
+
423
+ public TreeNode Parent { get; private set; } // 追加・削除の簡便さのため親ノードが欲しい
424
+
425
+
426
+
427
+ bool reentrancyCheck;
428
+
429
+
430
+
431
+ public TreeNode(string name) => Name = name;
432
+
433
+
434
+
435
+ public void Add(TreeNode child)
436
+
437
+ {
438
+
439
+ child.Parent = this;
440
+
441
+ Children.Add(child);
442
+
443
+ IsExpanded = true;
444
+
445
+ child.UpdateCheckState();
446
+
447
+ }
448
+
449
+ public void Remove(TreeNode child)
450
+
451
+ {
452
+
453
+ Children.Remove(child);
454
+
455
+ child.Parent = null;
456
+
457
+ if(0 < Children.Count) IsChecked = DetermineCheckState();
458
+
459
+ }
460
+
461
+
462
+
463
+ //チェック状態反映
464
+
465
+ // https://docs.telerik.com/devtools/wpf/controls/radtreeview/how-to/howto-tri-state-mvvm
466
+
467
+ void UpdateCheckState()
468
+
469
+ {
470
+
471
+ if(0 < Children.Count) UpdateChildrenCheckState();
472
+
473
+ if(Parent != null) Parent.IsChecked = Parent.DetermineCheckState();
474
+
475
+ }
476
+
477
+ void UpdateChildrenCheckState()
478
+
479
+ {
480
+
481
+ foreach(var c in Children)
482
+
483
+ {
484
+
485
+ if(IsChecked != null) c.IsChecked = IsChecked;
486
+
487
+ }
488
+
489
+ }
490
+
491
+ bool? DetermineCheckState()
492
+
493
+ {
494
+
495
+ var checkCount = Children.Count(x => x.IsChecked == true);
496
+
497
+ if(checkCount == Children.Count) return true;
498
+
499
+
500
+
501
+ var uncheckCount = Children.Count(x => x.IsChecked == false);
502
+
503
+ if(uncheckCount == Children.Count) return false;
504
+
505
+
506
+
507
+ return null;
508
+
509
+ }
510
+
511
+
512
+
513
+ // 子孫を全列挙(Children列挙でないので注意)
514
+
515
+ IEnumerable<TreeNode> Descendants(TreeNode node) => node.Children.Concat(node.Children.SelectMany(Descendants));
516
+
517
+ public IEnumerator<TreeNode> GetEnumerator() => Descendants(this).GetEnumerator();
518
+
519
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
520
+
521
+ }
522
+
523
+
524
+
525
+ class Observable : INotifyPropertyChanged
526
+
527
+ {
528
+
529
+ public event PropertyChangedEventHandler PropertyChanged;
530
+
531
+ protected void Set<T>(ref T storage, T value, [CallerMemberName] string name = null)
532
+
533
+ {
534
+
535
+ if(Equals(storage, value)) return;
536
+
537
+ storage = value;
538
+
539
+ OnPropertyChanged(name);
540
+
541
+ }
542
+
543
+ protected void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
544
+
545
+ }
546
+
547
+ class RelayCommand : ICommand
548
+
549
+ {
550
+
551
+ readonly Action exec;
552
+
553
+ readonly Func<bool> can;
554
+
555
+ public event EventHandler CanExecuteChanged;
556
+
557
+ public RelayCommand(Action e) : this(e, null) { }
558
+
559
+ public RelayCommand(Action e, Func<bool> c)
560
+
561
+ {
562
+
563
+ exec = e ?? throw new ArgumentNullException(nameof(e));
564
+
565
+ can = c;
566
+
567
+ }
568
+
569
+ public bool CanExecute(object _) => can == null || can();
570
+
571
+ public void Execute(object _) => exec();
572
+
573
+ public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
574
+
575
+ }
576
+
577
+ class RelayCommand<T> : ICommand
578
+
579
+ {
580
+
581
+ readonly Action<T> exec;
582
+
583
+ readonly Func<T, bool> can;
584
+
585
+ public event EventHandler CanExecuteChanged;
586
+
587
+ public RelayCommand(Action<T> e) : this(e, null) { }
588
+
589
+ public RelayCommand(Action<T> e, Func<T, bool> c)
590
+
591
+ {
592
+
593
+ exec = e ?? throw new ArgumentNullException(nameof(e));
594
+
595
+ can = c;
596
+
597
+ }
598
+
599
+ public bool CanExecute(object p) => can == null || can((T)p);
600
+
601
+ public void Execute(object p) => exec((T)p);
602
+
603
+ public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
604
+
605
+ }
110
606
 
111
607
  }
112
608
 
113
609
  ```
114
610
 
611
+ .xaml.csは初期状態
612
+
115
613
 
116
614
 
117
615
  ---
118
616
 
119
617
 
120
618
 
121
- TodoView.xaml
122
-
123
- ```xaml
124
-
125
- <UserControl
126
-
127
- x:Class="Questions292544.TodoView"
128
-
129
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
130
-
131
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
132
-
133
- xmlns:local="clr-namespace:Questions292544"
134
-
135
- MinWidth="200"
136
-
137
- MinHeight="200">
138
-
139
- <UserControl.Resources>
140
-
141
- <DataTemplate DataType="{x:Type local:TaskViewModel}">
142
-
143
- <local:TaskView />
144
-
145
- </DataTemplate>
146
-
147
-
148
-
149
- <DataTemplate x:Key="TaskEditView" DataType="{x:Type local:TaskViewModel}">
150
-
151
- <StackPanel>
152
-
153
- <TextBox Text="{Binding Title.Value, UpdateSourceTrigger=PropertyChanged}" />
154
-
155
- </StackPanel>
156
-
157
- </DataTemplate>
158
-
159
- </UserControl.Resources>
160
-
161
-
162
-
163
- <DockPanel>
164
-
165
- <DockPanel DockPanel.Dock="Bottom">
166
-
167
- <Button Command="{Binding AddCommand}" Content="New Task" />
168
-
169
- <ContentPresenter Content="{Binding Selected.Value}" ContentTemplate="{StaticResource TaskEditView}" />
170
-
171
- </DockPanel>
172
-
173
-
174
-
175
- <ListBox ItemsSource="{Binding TaskList}" SelectedItem="{Binding Selected.Value}">
176
-
177
- <ListBox.ItemContainerStyle>
178
-
179
- <Style TargetType="ListBoxItem">
180
-
181
- <Setter Property="HorizontalContentAlignment" Value="Stretch" />
182
-
183
- </Style>
184
-
185
- </ListBox.ItemContainerStyle>
186
-
187
- </ListBox>
188
-
189
- </DockPanel>
190
-
191
- </UserControl>
192
-
193
- ```
194
-
195
- TodoViewModel.cs
196
-
197
- ```C#
198
-
199
- using System;
200
-
201
- using System.Reactive.Linq;
202
-
203
- using Reactive.Bindings;
204
-
205
- using Reactive.Bindings.Extensions;
206
-
207
-
208
-
209
- namespace Questions292544
210
-
211
- {
212
-
213
- public class TodoViewModel
214
-
215
- {
216
-
217
- public ReadOnlyReactiveCollection<TaskViewModel> TaskList { get; }
218
-
219
- public ReactiveProperty<TaskViewModel> Selected { get; } = new ReactiveProperty<TaskViewModel>();
220
-
221
- public ReactiveCommand AddCommand { get; }
222
-
223
- public ReactiveCommand SaveCommand { get; }
224
-
225
-
226
-
227
- private readonly TodoModel todoModel;
228
-
229
-
230
-
231
- public TodoViewModel(TodoModel todoModel)
232
-
233
- {
234
-
235
- this.todoModel = todoModel;
236
-
237
- TaskList = todoModel.TaskList.ToReadOnlyReactiveCollection(x => new TaskViewModel(x));
238
-
239
- TaskList.ObserveAddChanged().Subscribe(x => Selected.Value = x);
240
-
241
-
242
-
243
- AddCommand = new ReactiveCommand().WithSubscribe(() => todoModel.TaskList.Add(new TaskModel()));
244
-
245
- SaveCommand = new ReactiveCommand().WithSubscribe(() => todoModel.Save());
246
-
247
- }
248
-
249
- }
250
-
251
- }
252
-
253
- ```
254
-
255
- TodoModel.cs
256
-
257
- ```C#
258
-
259
- using System.Collections.ObjectModel;
260
-
261
- using System.Runtime.Serialization;
262
-
263
- using System.Text;
264
-
265
- using System.Xml;
266
-
267
-
268
-
269
- namespace Questions292544
270
-
271
- {
272
-
273
- [DataContract(Namespace = "")]
274
-
275
- public class TodoModel
276
-
277
- {
278
-
279
- [DataMember] public ObservableCollection<TaskModel> TaskList { get; private set; } = new ObservableCollection<TaskModel>();
280
-
281
-
282
-
283
- private string path;
284
-
285
-
286
-
287
- public TodoModel()
288
-
289
- {
290
-
291
- TaskList.Add(new TaskModel { Title = "aaa", });
292
-
293
- TaskList.Add(new TaskModel { Title = "bbbbbb", });
294
-
295
- TaskList.Add(new TaskModel { Title = "ccccccccc", });
296
-
297
- }
298
-
299
-
300
-
301
- public static TodoModel Load(string path)
302
-
303
- {
304
-
305
- try
306
-
307
- {
308
-
309
- var serializer = new DataContractSerializer(typeof(TodoModel));
310
-
311
- using(var xr = XmlReader.Create(path))
312
-
313
- {
314
-
315
- var m = (TodoModel)serializer.ReadObject(xr);
316
-
317
- m.path = path;
318
-
319
- return m;
320
-
321
- }
322
-
323
- }
324
-
325
- catch { return new TodoModel { path = path, }; }
326
-
327
- }
328
-
329
-
330
-
331
- public void Save()
332
-
333
- {
334
-
335
- var serializer = new DataContractSerializer(typeof(TodoModel));
336
-
337
- var settings = new XmlWriterSettings
338
-
339
- {
340
-
341
- Encoding = new UTF8Encoding(false),
342
-
343
- Indent = true,
344
-
345
- };
346
-
347
- using(var xw = XmlWriter.Create(path, settings))
348
-
349
- {
350
-
351
- serializer.WriteObject(xw, this);
352
-
353
- }
354
-
355
- }
356
-
357
- }
358
-
359
- }
360
-
361
- ```
362
-
363
-
364
-
365
- ---
366
-
367
-
368
-
369
- TaskView.xaml
370
-
371
- ```xaml
372
-
373
- <UserControl
374
-
375
- x:Class="Questions292544.TaskView"
376
-
377
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
378
-
379
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
380
-
381
- MinWidth="100">
382
-
383
- <Grid>
384
-
385
- <Grid.ColumnDefinitions>
386
-
387
- <ColumnDefinition Width="Auto" />
388
-
389
- <ColumnDefinition Width="Auto" />
390
-
391
- <ColumnDefinition />
392
-
393
- </Grid.ColumnDefinitions>
394
-
395
- <CheckBox VerticalAlignment="Center" IsChecked="{Binding Done.Value}" />
396
-
397
- <TextBlock
398
-
399
- Grid.Column="1"
400
-
401
- Margin="10,0"
402
-
403
- xml:lang="ja"
404
-
405
- Text="{Binding Created.Value, StringFormat=d}" />
406
-
407
- <TextBlock Grid.Column="2" Text="{Binding Title.Value}" />
408
-
409
- </Grid>
410
-
411
- </UserControl>
412
-
413
- ```
414
-
415
- TaskViewModel.cs
416
-
417
- ```C#
418
-
419
- using System;
420
-
421
- using Reactive.Bindings;
422
-
423
-
424
-
425
- namespace Questions292544
426
-
427
- {
428
-
429
- public class TaskViewModel
430
-
431
- {
432
-
433
- public ReactiveProperty<bool> Done { get; }
434
-
435
- public ReactiveProperty<DateTime> Created { get; }
436
-
437
- public ReactiveProperty<string> Title { get; }
438
-
439
-
440
-
441
- public TaskViewModel(TaskModel task)
442
-
443
- {
444
-
445
- Done = ReactiveProperty.FromObject(task, x => x.Done);
446
-
447
- Created = ReactiveProperty.FromObject(task, x => x.Created);
619
+ `TreeNode`は厳密には`ViewModel`になるかもしれませんが、ライブラリなしではめんどくさいのでVM・M兼用です^^;
448
-
449
- Title = ReactiveProperty.FromObject(task, x => x.Title);
450
-
451
- }
452
-
453
- }
454
-
455
- }
456
-
457
- ```
458
-
459
- TaskModel.cs
460
-
461
- ```C#
462
-
463
- using System;
464
-
465
- using System.Runtime.Serialization;
466
-
467
-
468
-
469
- namespace Questions292544
470
-
471
- {
472
-
473
- [DataContract(Namespace = "")]
474
-
475
- public class TaskModel
476
-
477
- {
478
-
479
- [DataMember] public bool Done { get; set; }
480
-
481
- [DataMember] public DateTime Created { get; private set; }
482
-
483
- [DataMember] public string Title { get; set; }
484
-
485
-
486
-
487
- public TaskModel() => Created = DateTime.Now;
488
-
489
- }
490
-
491
- }
492
-
493
- ```
494
-
495
-
496
-
497
- xaml.cs(コードビハインド)はすべて初期状態
498
-
499
- コード量を抑えつつ、細切れにするのは難しいですね^^;