Dreamine.UI.Wpf.Controls 1.0.1
Dreamine.UI.Wpf.Controls 사용자 인터페이스 기능과 구성 요소를 제공합니다.
로딩중...
검색중...
일치하는것 없음
ViewLoader.cs
이 파일의 문서화 페이지로 가기
1using System;
2using System.Diagnostics;
3using System.Linq;
4using System.Reflection;
5using System.Windows;
6using System.Windows.Controls;
7using System.Windows.Navigation;
8using Dreamine.MVVM.Core;
9using Dreamine.MVVM.ViewModels;
10
12{
21 public static class ViewLoader
22 {
31 public class LoadedViewInfo
32 {
41 public UserControl? View { get; set; }
42
51 public FrameworkElement? FrameworkView { get; set; }
52
61 public Type? ViewModelType { get; set; }
62
71 public bool IsPopup { get; set; }
72
81 public string? UniqueKey { get; set; }
82 }
83
132 public static LoadedViewInfo LoadViewWithViewModel(string typeName, bool useSingletonView)
133 {
134 // \brief Parse popup suffix
135 bool isPopup = typeName.EndsWith("_Popup", StringComparison.OrdinalIgnoreCase);
136 string actualTypeName = isPopup ? typeName[..^6] : typeName;
137
138 // \brief 1) Resolve View type
139 var viewType =
140 Type.GetType(actualTypeName, throwOnError: false, ignoreCase: true)
141 ?? AppDomain.CurrentDomain.GetAssemblies()
142 .SelectManySafe(a => a.GetTypesSafe())
143 .FirstOrDefault(t =>
144 t.FullName?.Equals(actualTypeName, StringComparison.OrdinalIgnoreCase) == true ||
145 t.Name.Equals(actualTypeName.Split('.').Last(), StringComparison.OrdinalIgnoreCase));
146
147 // \brief 2) Resolve ViewModel type (FIXED)
148 Type? viewModelType = ResolveViewModelType_Strict(actualTypeName, viewType);
149
150 // \brief ViewModel exists => reset DataContext strategy may differ
151 bool resetDataContext = viewModelType != null;
152
153 object? vmInstance = null;
154 string? uniqueKey = null;
155
156 // \brief 3) Create/Resolve ViewModel instance
157 if (viewModelType != null)
158 {
159 if (useSingletonView)
160 {
161 vmInstance = DMContainer.Resolve(viewModelType) ?? Activator.CreateInstance(viewModelType);
162 }
163 else
164 {
165 // \brief Multi instance unique key
166 long typeIndex = ViewModelKeyCache.GetOrIncrement(typeName);
167 uniqueKey = $"{typeName}_{typeIndex:D2}";
168
169 vmInstance = Activator.CreateInstance(viewModelType);
170 }
171 }
172
173 // \brief 4) Create FrameworkElement
174 FrameworkElement framework = ResolveFrameworkElement(viewType, actualTypeName, resetDataContext);
175
176 // \brief 5) Apply DataContext when VM exists
177 if (vmInstance != null)
178 {
179 ApplyDataContext(framework, vmInstance);
180 }
181
182 // \brief 6) Ensure wrapper as UserControl
183 UserControl wrapper;
184 if (framework is UserControl uc)
185 {
186 wrapper = uc;
187 }
188 else
189 {
190 wrapper = new EmbeddedHostControl(framework);
191
192 // \note Ensure wrapper has same DataContext as the hosted framework
193 if (wrapper.DataContext == null)
194 {
195 wrapper.DataContext = framework.DataContext;
196 }
197 }
198
199 return new LoadedViewInfo
200 {
201 View = wrapper,
202 FrameworkView = framework,
203 ViewModelType = viewModelType,
204 IsPopup = isPopup,
205 UniqueKey = uniqueKey
206 };
207 }
208
241 private static string StripViewSuffix(string name)
242 {
243 const string suffix = "View";
244 if (name.Length > suffix.Length && name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
245 return name[..^suffix.Length];
246 return name;
247 }
248
289 private static Type? ResolveViewModelType_Strict(string actualTypeName, Type? viewType)
290 {
291 // \brief Candidate full names
292 var candidates = new List<string>();
293
294 // \brief Common: same namespace + ViewModel suffix
295 candidates.Add(actualTypeName + "ViewModel");
296 candidates.Add(StripViewSuffix(actualTypeName) + "ViewModel");
297
298 // \brief Pages -> ViewModels convention
299 if (actualTypeName.Contains(".Pages.", StringComparison.OrdinalIgnoreCase))
300 {
301 var pagesToVm = actualTypeName.Replace(".Pages.", ".ViewModels.", StringComparison.OrdinalIgnoreCase);
302 candidates.Add(pagesToVm + "ViewModel");
303 candidates.Add(StripViewSuffix(pagesToVm) + "ViewModel");
304 }
305
306 // \brief Views -> ViewModels convention (optional, but common)
307 if (actualTypeName.Contains(".Views.", StringComparison.OrdinalIgnoreCase))
308 {
309 var viewsToVm = actualTypeName.Replace(".Views.", ".ViewModels.", StringComparison.OrdinalIgnoreCase);
310 candidates.Add(viewsToVm + "ViewModel");
311 candidates.Add(StripViewSuffix(viewsToVm) + "ViewModel");
312 }
313
314 // \brief If viewType is known, add "Namespace.ViewModels.{ViewName}ViewModel"
315 if (viewType != null)
316 {
317 string viewNs = viewType.Namespace ?? string.Empty;
318 string viewName = viewType.Name;
319 string viewNameNoSuffix = StripViewSuffix(viewName);
320 if (!string.IsNullOrWhiteSpace(viewNs))
321 {
322 candidates.Add($"{viewNs}.ViewModels.{viewName}ViewModel");
323 candidates.Add($"{viewNs}.ViewModels.{viewNameNoSuffix}ViewModel");
324 candidates.Add($"{viewNs}.{viewName}ViewModel");
325 candidates.Add($"{viewNs}.{viewNameNoSuffix}ViewModel");
326
327 // \brief Sibling "Views" -> "ViewModels" namespace swap
328 if (viewNs.Contains(".Views", StringComparison.OrdinalIgnoreCase))
329 {
330 string swappedNs = viewNs.Replace(".Views", ".ViewModels", StringComparison.OrdinalIgnoreCase);
331 candidates.Add($"{swappedNs}.{viewName}ViewModel");
332 candidates.Add($"{swappedNs}.{viewNameNoSuffix}ViewModel");
333 }
334 }
335
336 // \brief Short name candidates
337 candidates.Add(viewName + "ViewModel");
338 candidates.Add(viewNameNoSuffix + "ViewModel");
339 }
340
341 // \brief De-duplicate
342 candidates = candidates
343 .Where(x => !string.IsNullOrWhiteSpace(x))
344 .Distinct(StringComparer.OrdinalIgnoreCase)
345 .ToList();
346
347 // \brief Search with strict filtering
348 foreach (var cand in candidates)
349 {
350 var t = AppDomain.CurrentDomain.GetAssemblies()
351 .SelectManySafe(a => a.GetTypesSafe())
352 .FirstOrDefault(x =>
353 x.FullName?.Equals(cand, StringComparison.OrdinalIgnoreCase) == true ||
354 x.Name.Equals(cand.Split('.').Last(), StringComparison.OrdinalIgnoreCase));
355
356 if (t == null)
357 {
358 continue;
359 }
360
361 // \brief Must be a concrete class
362 if (!t.IsClass || t.IsAbstract)
363 {
364 continue;
365 }
366
367 // \brief Prevent selecting View type as ViewModel
368 if (typeof(FrameworkElement).IsAssignableFrom(t))
369 {
370 continue;
371 }
372
373 // \brief Strong filter: must be ViewModelBase (your framework)
374 if (!typeof(ViewModelBase).IsAssignableFrom(t))
375 {
376 continue;
377 }
378
379 return t;
380 }
381
382 return null;
383 }
384
433 private static FrameworkElement ResolveFrameworkElement(Type? viewType, string typeName, bool resetDataContext)
434 {
435 if (viewType == null || viewType.IsAbstract)
436 {
437 return new TextBlock
438 {
439 Text = $"[No View Type: {typeName}]",
440 Margin = new Thickness(8)
441 };
442 }
443
444 object? instance = null;
445
446 // 1차: VsControls를 통한 생성 시도 (항상 새 인스턴스)
447 try
448 {
449 instance = DMContainer.Resolve(viewType);
450 }
451 catch (Exception ex)
452 {
453 System.Diagnostics.Debug.WriteLine($"[ViewLoader] VsContainer.Resolve 실패: {typeName} - {ex.Message}");
454 }
455
456 // 2차: 컨테이너 생성 실패 시 Activator.CreateInstance로 폴백
457 if (instance == null)
458 {
459 try
460 {
461 instance = Activator.CreateInstance(viewType);
462 }
463 catch (Exception ex)
464 {
465 return new TextBlock
466 {
467 Text = $"[CreateInstance Failed: {typeName}] {ex.Message}",
468 Margin = new Thickness(8)
469 };
470 }
471 }
472
473 switch (instance)
474 {
475 // --- UserControl 그대로 사용 ---
476 case UserControl uc:
477 {
478 if (resetDataContext)
479 {
480 uc.DataContext = null;
481 }
482 return uc;
483 }
484
485 // --- Page → Frame 호스팅 ---
486 case Page page:
487 {
488 if (resetDataContext)
489 {
490 page.DataContext = null;
491 }
492
493 var frame = new Frame
494 {
495 Content = page,
496 NavigationUIVisibility = NavigationUIVisibility.Hidden,
497 JournalOwnership = JournalOwnership.OwnsJournal
498 };
499
500 if (resetDataContext)
501 {
502 frame.DataContext = null;
503 }
504
505 return frame;
506 }
507
508 // --- Window → ContentControl 호스팅 ---
509 case Window win:
510 {
511 if (win.Content is FrameworkElement content)
512 {
513 if (resetDataContext)
514 {
515 content.DataContext = null;
516 win.DataContext = null;
517 }
518
519 win.Content = null;
520 var host = new ContentControl
521 {
522 Content = content
523 };
524
525 if (resetDataContext)
526 {
527 host.DataContext = null;
528 }
529
530 MergeResources(host.Resources, win.Resources);
531 return host;
532 }
533
534 return new TextBlock
535 {
536 Text = $"[Window has no Content: {typeName}]",
537 Margin = new Thickness(8)
538 };
539 }
540
541 // --- 기타 FrameworkElement ---
542 case FrameworkElement fe:
543 {
544 if (resetDataContext)
545 {
546 fe.DataContext = null;
547 }
548 return fe;
549 }
550
551 default:
552 {
553 return new TextBlock
554 {
555 Text = $"[Unknown View Type: {viewType.FullName}]",
556 Margin = new Thickness(8)
557 };
558 }
559 }
560 }
561
594 private static void ApplyDataContext(FrameworkElement root, object vm)
595 {
596 if (root == null || vm == null)
597 {
598 return;
599 }
600
601 // --- Frame 전용 처리: Page 로드 타이밍 보장 ---
602 if (root is Frame fr)
603 {
604 // Frame 자신에도 ViewModel 설정
605 fr.DataContext = vm;
606
607 // 로컬: 실제 컨텐츠에 주입
608#pragma warning disable CS1587 // Doxygen documents local functions; the C# compiler does not attach XML docs to them.
627 void TrySet(FrameworkElement fe)
628 {
629 if (fe != null)
630 {
631 fe.DataContext = vm;
632 }
633 }
634#pragma warning restore CS1587
635
636 // 로컬: Loaded 이후 1회성 주입
637#pragma warning disable CS1587 // Doxygen documents local functions; the C# compiler does not attach XML docs to them.
664 void OnLoaded(object? s, RoutedEventArgs e)
665 {
666 if (s is FrameworkElement fe)
667 {
668 fe.Loaded -= OnLoaded;
669 TrySet(fe);
670 }
671 }
672#pragma warning restore CS1587
673
674 // 로컬: 네비게이션 직후 1회성 주입
675#pragma warning disable CS1587 // Doxygen documents local functions; the C# compiler does not attach XML docs to them.
702 void OnNavigated(object? s, NavigationEventArgs e)
703 {
704 fr.Navigated -= OnNavigated;
705
706 if (fr.Content is FrameworkElement fe)
707 {
708 if (!fe.IsLoaded)
709 {
710 fe.Loaded += OnLoaded;
711 }
712
713 TrySet(fe);
714 }
715 }
716#pragma warning restore CS1587
717
718 // 현재 시점에 컨텐츠가 있으면 즉시/Loaded에서 주입
719 if (fr.Content is FrameworkElement current)
720 {
721 if (!current.IsLoaded)
722 {
723 current.Loaded += OnLoaded;
724 }
725
726 TrySet(current);
727 }
728 else
729 {
730 // 아직 Page가 없음 → 다음 네비게이션에서 주입
731 fr.Navigated += OnNavigated;
732 }
733
734 return;
735 }
736
737 // --- ContentControl 호스트 처리 ---
738 if (root is ContentControl cc && cc.Content is FrameworkElement innerCc)
739 {
740 cc.DataContext = vm;
741 innerCc.DataContext = vm;
742 return;
743 }
744
745 // --- 일반 FrameworkElement 처리 ---
746 root.DataContext = vm;
747 }
748
749
790 private static void MergeResources(ResourceDictionary target, ResourceDictionary source)
791 {
792 foreach (var rd in source.MergedDictionaries)
793 target.MergedDictionaries.Add(rd);
794
795 foreach (var key in source.Keys)
796 if (!target.Contains(key))
797 target.Add(key, source[key]);
798 }
799
832 private static Type[] GetTypesSafe(this Assembly asm)
833 {
834 try { return asm.GetTypes(); }
835 catch (ReflectionTypeLoadException ex) { return ex.Types.Where(t => t != null).ToArray()!; }
836 }
837
886 private static IEnumerable<T> SelectManySafe<T>(this IEnumerable<Assembly> assemblies, Func<Assembly, IEnumerable<T>> selector)
887 {
888 foreach (var a in assemblies)
889 {
890 if (a.IsDynamic) continue;
891 IEnumerable<T>? res = null;
892 try { res = selector(a); }
893 catch (Exception ex) { Debug.WriteLine($"[ViewLoader] Assembly scan 실패 ({a.FullName}): {ex.Message}"); }
894 if (res != null) foreach (var x in res) yield return x;
895 }
896 }
897
906 private sealed class EmbeddedHostControl : UserControl
907 {
924 public EmbeddedHostControl(FrameworkElement inner)
925 {
926 Content = inner;
927 }
928 }
929 }
930}
static Type[] GetTypesSafe(this Assembly asm)
static FrameworkElement ResolveFrameworkElement(Type? viewType, string typeName, bool resetDataContext)
static string StripViewSuffix(string name)
static void MergeResources(ResourceDictionary target, ResourceDictionary source)
static void ApplyDataContext(FrameworkElement root, object vm)
static LoadedViewInfo LoadViewWithViewModel(string typeName, bool useSingletonView)
static IEnumerable< T > SelectManySafe< T >(this IEnumerable< Assembly > assemblies, Func< Assembly, IEnumerable< T > > selector)
static ? Type ResolveViewModelType_Strict(string actualTypeName, Type? viewType)