回答編集履歴

1

コードを追加

2025/03/28 08:48

投稿

kikiinu
kikiinu

スコア24

test CHANGED
@@ -1 +1,303 @@
1
1
  色々考えるより数行で対応できたのでコードビハインドで実装しました
2
+
3
+ まず、自作のTextBlockErrorMessageを作成しました
4
+ ``` C#
5
+ コード
6
+ using System;
7
+ using System.Collections.Generic;
8
+ using System.ComponentModel;
9
+ using System.Diagnostics;
10
+ using System.Linq;
11
+ using System.Text;
12
+ using System.Threading.Tasks;
13
+ using System.Windows;
14
+ using System.Windows.Controls;
15
+ using System.Windows.Data;
16
+ using System.Windows.Documents;
17
+ using System.Windows.Input;
18
+ using System.Windows.Media;
19
+ using System.Windows.Media.Imaging;
20
+ using System.Windows.Navigation;
21
+ using System.Windows.Shapes;
22
+
23
+ namespace SeikouCustomControl
24
+ {
25
+ /// <summary>
26
+ /// このカスタム コントロールを XAML ファイルで使用するには、手順 1a または 1b の後、手順 2 に従います。
27
+ ///
28
+ /// 手順 1a) 現在のプロジェクトに存在する XAML ファイルでこのカスタム コントロールを使用する場合
29
+ /// この XmlNamespace 属性を使用場所であるマークアップ ファイルのルート要素に
30
+ /// 追加します:
31
+ ///
32
+ /// xmlns:MyNamespace="clr-namespace:SeikouCustomControl"
33
+ ///
34
+ ///
35
+ /// 手順 1b) 異なるプロジェクトに存在する XAML ファイルでこのカスタム コントロールを使用する場合
36
+ /// この XmlNamespace 属性を使用場所であるマークアップ ファイルのルート要素に
37
+ /// 追加します:
38
+ ///
39
+ /// xmlns:MyNamespace="clr-namespace:SeikouCustomControl;assembly=SeikouCustomControl"
40
+ ///
41
+ /// また、XAML ファイルのあるプロジェクトからこのプロジェクトへのプロジェクト参照を追加し、
42
+ /// リビルドして、コンパイル エラーを防ぐ必要があります:
43
+ ///
44
+ /// ソリューション エクスプローラーで対象のプロジェクトを右クリックし、
45
+ /// [参照の追加] の [プロジェクト] を選択してから、このプロジェクトを参照し、選択します。
46
+ ///
47
+ ///
48
+ /// 手順 2)
49
+ /// コントロールを XAML ファイルで使用します。
50
+ ///
51
+ /// <MyNamespace:TextBlockErrorMessage/>
52
+ ///
53
+ /// </summary>
54
+ public class TextBlockErrorMessage : TextBlock
55
+ {
56
+ static TextBlockErrorMessage()
57
+ {
58
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBlockErrorMessage), new FrameworkPropertyMetadata(typeof(TextBlockErrorMessage)));
59
+
60
+ }
61
+
62
+ public TextBlockErrorMessage()
63
+ {
64
+ var descripter = DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, typeof(TextBlock));
65
+ this.Visibility = Visibility.Collapsed;
66
+ descripter.AddValueChanged(this, OnTextChanged);
67
+ }
68
+
69
+ private static void OnTextChanged(object? sender, EventArgs e)
70
+ {
71
+ var textBlock = (TextBlock)sender;
72
+ if (string.IsNullOrEmpty(textBlock.Text))
73
+ {
74
+ textBlock.Visibility = Visibility.Collapsed;
75
+ }
76
+ else
77
+ {
78
+ textBlock.Visibility = Visibility.Visible;
79
+ }
80
+ }
81
+
82
+ }
83
+ }
84
+
85
+ ```
86
+ 自作のTextBlockErrorMessageを配置します
87
+
88
+ ```WPF XAML
89
+ <Window
90
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
91
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
92
+ xmlns:local="clr-namespace:Test" xmlns:sys="http://schemas.microsoft.com/winfx/2009/xaml"
93
+ xmlns:scc="clr-namespace:SeikouCustomControl;assembly=SeikouCustomControl"
94
+ x:Class="Test.MainWindow"
95
+ Title="MainWindow"
96
+ Width="525"
97
+ Height="350">
98
+ <Window.Resources>
99
+ </Window.Resources>
100
+ <Window.DataContext>
101
+ <local:MainWindowViewModel />
102
+ </Window.DataContext>
103
+ <Grid>
104
+ <Grid.ColumnDefinitions>
105
+ <ColumnDefinition Width="*"/>
106
+ <ColumnDefinition Width="Auto"/>
107
+ <ColumnDefinition Width="*"/>
108
+ <ColumnDefinition Width="*"/>
109
+ </Grid.ColumnDefinitions>
110
+ <Grid.RowDefinitions>
111
+ <RowDefinition Height="auto"/>
112
+ <RowDefinition Height="auto"/>
113
+ <RowDefinition Height="auto"/>
114
+ </Grid.RowDefinitions>
115
+
116
+ <Label Grid.Row="0" Grid.Column="1" Content="年齢" />
117
+ <TextBox Grid.Row="0" Grid.Column="2"
118
+ Text="{Binding InputString, UpdateSourceTrigger=PropertyChanged}"/>
119
+
120
+ <scc:TextBlockErrorMessage Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="100"
121
+ Text="{Binding InputStringErrorMessage}"
122
+ />
123
+ </Grid>
124
+ </Window>
125
+ ```
126
+
127
+ ViewModelにプロパティを作ります
128
+
129
+ ```C#
130
+ using System.ComponentModel.DataAnnotations;
131
+ using System.Runtime.CompilerServices;
132
+ using Test;
133
+
134
+ namespace Test
135
+ {
136
+ public class MainWindowViewModel : ValidateableBase
137
+ {
138
+ private string inputString = string.Empty;
139
+ [Required(ErrorMessage = "年齢 : 何か入力してください")]
140
+ [StringLength(10, ErrorMessage = "年齢 : \r\n10文字以内で\r\n入力してください")]
141
+ public string InputString
142
+ {
143
+ get { return inputString; }
144
+ set
145
+ {
146
+ SetPropertyWithErrorMessage(ref this.inputString, value);
147
+ }
148
+ }
149
+
150
+ private string _InputStringErrorMessage = string.Empty;
151
+ public string InputStringErrorMessage
152
+ {
153
+ get { return _InputStringErrorMessage; }
154
+ private set { this.SetProperty(ref _InputStringErrorMessage, value); }
155
+ }
156
+
157
+ public MainWindowViewModel()
158
+ {
159
+ }
160
+ }
161
+ }
162
+
163
+ ```
164
+
165
+ 次に汎用性を持たせてSetPropertyWithErrorMessageメソッドを作りました
166
+ これがベストなのかはわかりません。
167
+
168
+ ```C#
169
+ using System.Collections;
170
+ using System.ComponentModel;
171
+ using System.ComponentModel.DataAnnotations;
172
+ using System.Reflection;
173
+ using System.Runtime.CompilerServices;
174
+
175
+ namespace Test
176
+ {
177
+ public abstract class ValidateableBase : BindableBase, INotifyDataErrorInfo
178
+ {
179
+ /// <summary>
180
+ /// プロパティが既に目的の値と一致しているかどうかを確認します。必要な場合のみ、
181
+ /// プロパティを設定し、リスナーに通知します。
182
+ /// その後、プロパティの入力値検証を行います。
183
+ /// </summary>
184
+ /// <typeparam name="T">プロパティの型。</typeparam>
185
+ /// <param name="storage">get アクセス操作子と set アクセス操作子両方を使用したプロパティへの参照。</param>
186
+ /// <param name="value">プロパティに必要な値。</param>
187
+ /// <param name="propertyName">リスナーに通知するために使用するプロパティの名前。
188
+ /// この値は省略可能で、
189
+ /// CallerMemberName をサポートするコンパイラから呼び出す場合に自動的に指定できます。</param>
190
+ /// <returns>値が変更された場合は true、既存の値が目的の値に一致した場合は
191
+ /// false です。</returns>
192
+ protected override bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
193
+ {
194
+ var isChanged = base.SetProperty(ref storage, value, propertyName);
195
+ if (isChanged)
196
+ this.ValidateProperty(value, propertyName);
197
+
198
+ return isChanged;
199
+ }
200
+
201
+ protected Dictionary<string, PropertyInfo?> _properties = new Dictionary<string, PropertyInfo?>();
202
+
203
+ protected void SetPropertyWithErrorMessage<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
204
+ {
205
+ this.SetProperty(ref storage, value, propertyName);
206
+
207
+ var errorMessage = GetErrorMessage("\r\n", propertyName);
208
+
209
+ // プロパティ情報の取得
210
+ if (!_properties.TryGetValue(propertyName, out var property))
211
+ {
212
+ property = this.GetType().GetProperty(propertyName + "ErrorMessage");
213
+ _properties[propertyName] = property;
214
+ }
215
+
216
+ // インスタンスに値を設定
217
+ property?.SetValue(this, errorMessage);
218
+ }
219
+ private string GetErrorMessage(string? messageSeparator, String propertyName)
220
+ {
221
+ var errors = (List<string>)GetErrors(propertyName);
222
+ if (errors != null)
223
+ {
224
+ var ss = new string[errors.Count];
225
+ errors.CopyTo(ss);
226
+ return string.Join(messageSeparator, ss);
227
+ }
228
+
229
+ return string.Empty;
230
+ }
231
+
232
+ protected void ValidateProperty(object value, [CallerMemberName] string propertyName = null)
233
+ {
234
+ var context = new ValidationContext(this) { MemberName = propertyName };
235
+ var validationErrors = new List<ValidationResult>();
236
+ if (!Validator.TryValidateProperty(value, context, validationErrors))
237
+ {
238
+ var errors = validationErrors.Select(error => error.ErrorMessage);
239
+ foreach (var error in errors)
240
+ {
241
+ AddError(propertyName, error);
242
+ }
243
+ }
244
+ else
245
+ {
246
+ RemoveError(propertyName);
247
+ }
248
+ }
249
+
250
+ #region 発生中のエラーを保持する処理を実装
251
+ readonly Dictionary<string, List<string>> _currentErrors = new Dictionary<string, List<string>>();
252
+
253
+ protected void AddError(string propertyName, string error)
254
+ {
255
+ if (!_currentErrors.ContainsKey(propertyName))
256
+ _currentErrors[propertyName] = new List<string>();
257
+
258
+ if (!_currentErrors[propertyName].Contains(error))
259
+ {
260
+ _currentErrors[propertyName].Add(error);
261
+ OnErrorsChanged(propertyName);
262
+ }
263
+ }
264
+
265
+ protected void RemoveError(string propertyName)
266
+ {
267
+ if (_currentErrors.ContainsKey(propertyName))
268
+ _currentErrors.Remove(propertyName);
269
+
270
+ OnErrorsChanged(propertyName);
271
+ }
272
+ #endregion
273
+
274
+ private void OnErrorsChanged(string propertyName)
275
+ {
276
+ var h = this.ErrorsChanged;
277
+ if (h != null)
278
+ {
279
+ h(this, new DataErrorsChangedEventArgs(propertyName));
280
+ }
281
+ }
282
+
283
+ #region INotifyDataErrorInfoの実装
284
+ public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
285
+
286
+ public IEnumerable GetErrors(string propertyName)
287
+ {
288
+ if (string.IsNullOrEmpty(propertyName) ||
289
+ !_currentErrors.ContainsKey(propertyName))
290
+ return null;
291
+
292
+ return _currentErrors[propertyName];
293
+ }
294
+
295
+ public bool HasErrors
296
+ {
297
+ get { return _currentErrors.Count > 0; }
298
+ }
299
+ #endregion
300
+ }
301
+ }
302
+
303
+ ```