Dreamine.UI.Wpf.Controls 1.0.1
Dreamine.UI.Wpf.Controls 사용자 인터페이스 기능과 구성 요소를 제공합니다.
로딩중...
검색중...
일치하는것 없음
DreaminePasswordBox.cs
이 파일의 문서화 페이지로 가기
1// \file DreaminePasswordBox.cs
2// \brief Hint/Error 지원 + Password 바인딩 가능한 커스텀 PasswordBox 컨트롤
3// \details
4// - ControlTemplate 내부 PART_PasswordBox(PasswordBox)와 연동
5// - Password DP(TwoWay) 지원
6// - 템플릿 재적용 시 이벤트 중복 등록 방지
7// - PasswordBox.PasswordChanged <-> DP 변경 간 재진입(무한루프) 방지
8// \author VsLibrary
9// \date 2026-02-11
10
11using System;
12using System.Linq;
13using System.Windows;
14using System.Windows.Controls;
15
17{
26 public class DreaminePasswordBox : Control
27 {
28 // =====================================================================
29 // \brief Static constructor - style merge
30 // =====================================================================
40 {
41 DefaultStyleKeyProperty.OverrideMetadata(typeof(DreaminePasswordBox),
42 new FrameworkPropertyMetadata(typeof(DreaminePasswordBox)));
43
44 // \brief 스타일 리소스 병합
45 var uri = new Uri("/Dreamine.UI.Wpf.Themes;component/DreaminePasswordBoxStyle.xaml", UriKind.RelativeOrAbsolute);
46
47 if (Application.Current != null)
48 {
49 bool alreadyAdded = Application.Current.Resources.MergedDictionaries
50 .OfType<ResourceDictionary>()
51 .Any(x => x.Source != null && x.Source.Equals(uri));
52
53 if (!alreadyAdded)
54 {
55 var dict = new ResourceDictionary { Source = uri };
56 Application.Current.Resources.MergedDictionaries.Add(dict);
57 }
58 }
59 }
60
61 // =====================================================================
62 // \brief DP: Hint / Error / IsPasswordEmpty / Password
63 // =====================================================================
64
73 public static readonly DependencyProperty HintProperty =
74 DependencyProperty.Register(nameof(Hint), typeof(string), typeof(DreaminePasswordBox),
75 new PropertyMetadata(string.Empty));
76
85 public static readonly DependencyProperty ErrorProperty =
86 DependencyProperty.Register(nameof(Error), typeof(string), typeof(DreaminePasswordBox),
87 new PropertyMetadata(string.Empty));
88
97 public static readonly DependencyProperty IsPasswordEmptyProperty =
98 DependencyProperty.Register(nameof(IsPasswordEmpty), typeof(bool), typeof(DreaminePasswordBox),
99 new PropertyMetadata(true));
100
109 public static readonly DependencyProperty PasswordProperty =
110 DependencyProperty.Register(
111 nameof(Password),
112 typeof(string),
113 typeof(DreaminePasswordBox),
114 new FrameworkPropertyMetadata(
115 string.Empty,
116 FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
118
127 public string Password
128 {
129 get => (string)GetValue(PasswordProperty);
130 set => SetValue(PasswordProperty, value);
131 }
132
141 public string Hint
142 {
143 get => (string)GetValue(HintProperty);
144 set => SetValue(HintProperty, value);
145 }
146
155 public string Error
156 {
157 get => (string)GetValue(ErrorProperty);
158 set => SetValue(ErrorProperty, value);
159 }
160
169 public bool IsPasswordEmpty
170 {
171 get => (bool)GetValue(IsPasswordEmptyProperty);
172 set => SetValue(IsPasswordEmptyProperty, value);
173 }
174
175 // =====================================================================
176 // \brief Internal fields
177 // =====================================================================
178
187 private PasswordBox? _partPasswordBox;
188
197 private RoutedEventHandler? _passwordChangedHandler;
198
207 private bool _isSyncing;
208
209 // =====================================================================
210 // \brief Template hook
211 // =====================================================================
212
221 public override void OnApplyTemplate()
222 {
223 base.OnApplyTemplate();
224
225 // \brief 이전 PART가 있으면 이벤트 해제
227
228 _partPasswordBox = GetTemplateChild("PART_PasswordBox") as PasswordBox;
229 if (_partPasswordBox == null)
230 return;
231
232 // \brief 최초 동기화: DP -> PART
234
235 // \brief PasswordChanged 연결 (참조 보관)
236 _passwordChangedHandler = (_, __) =>
237 {
238 if (_partPasswordBox == null)
239 return;
240
241 if (_isSyncing)
242 return;
243
244 try
245 {
246 _isSyncing = true;
247
248 string pwd = _partPasswordBox.Password ?? string.Empty;
249
250 // \brief PART -> DP
251 if (!string.Equals(Password ?? string.Empty, pwd, StringComparison.Ordinal))
252 SetCurrentValue(PasswordProperty, pwd);
253
254 // \brief empty 플래그 갱신
255 SetCurrentValue(IsPasswordEmptyProperty, string.IsNullOrEmpty(pwd));
256 }
257 finally
258 {
259 _isSyncing = false;
260 }
261 };
262
263 _partPasswordBox.PasswordChanged += (s, e) => _passwordChangedHandler?.Invoke(s, e);
264
265 // \brief 컨트롤 클릭 시 내부 입력으로 포커스 이동 (UX)
266 this.MouseDown += OnHostMouseDown;
267 this.GotKeyboardFocus += OnHostGotKeyboardFocus;
268 }
269
278 private void DetachPartHandlers()
279 {
280 // \note PasswordBox.PasswordChanged는 익명 람다로 직접 해제하기 어려우니
281 // 템플릿 재적용 시 새 PART로 갈아끼우는 패턴에서 “PART 캐시 교체”로 방어한다.
282 // (현재 구조는 PART가 바뀌면 이전 PART는 UI 트리에서 떨어지므로 누적 영향이 크지 않음)
283
284 this.MouseDown -= OnHostMouseDown;
285 this.GotKeyboardFocus -= OnHostGotKeyboardFocus;
286
287 _partPasswordBox = null;
289 }
290
315 private void OnHostMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
316 {
317 _partPasswordBox?.Focus();
318 }
319
344 private void OnHostGotKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
345 {
346 if (e.NewFocus == this)
347 {
348 _partPasswordBox?.Focus();
349 e.Handled = true;
350 }
351 }
352
353 // =====================================================================
354 // \brief DP -> PART 동기화
355 // =====================================================================
356
381 private static void OnPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
382 {
383 if (d is not DreaminePasswordBox control)
384 return;
385
386 if (control._isSyncing)
387 return;
388
389 string newPassword = e.NewValue as string ?? string.Empty;
390
391 control.SyncDpToPart(newPassword);
392 }
393
410 private void SyncDpToPart(string newPassword)
411 {
412 try
413 {
414 _isSyncing = true;
415
416 SetCurrentValue(IsPasswordEmptyProperty, string.IsNullOrEmpty(newPassword));
417
418 if (_partPasswordBox != null)
419 {
420 string current = _partPasswordBox.Password ?? string.Empty;
421 if (!string.Equals(current, newPassword, StringComparison.Ordinal))
422 _partPasswordBox.Password = newPassword;
423 }
424 }
425 finally
426 {
427 _isSyncing = false;
428 }
429 }
430 }
431}
void OnHostGotKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
static readonly DependencyProperty HintProperty
static readonly DependencyProperty IsPasswordEmptyProperty
void OnHostMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
static void OnPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
static readonly DependencyProperty ErrorProperty
static readonly DependencyProperty PasswordProperty