- 학습 순서 및 접근 방법
- Caliburn.Micro , MVVM 템플릿
- App.xaml 수정
- Bootstrapper 클래스 작성
- ShellViewModels.cs
- ShellView.xaml 윈도우
- 참고 : Helper Class
Microsoft의 .NET Core 3 정식 버전 출시가 눈앞에 다가왔다. 아마 1~2개월 안으로 정식 버전이 나오고 바로 3.1 LTS 버전도 발표할 것이다. Core 3 부터 윈도우 애플리케이션을 위한 WinForm과 WPF 개발을 지원한다. WPF 기반으로 애플리케이션을 개발하면서 기본적인 방법론을 정리해보았다. 특히 MVVM(Model-View-ViewModel) 패턴에 있어 Caliburn.Micro 패키지는 매우 유용하다. Core는 기존의 .NET Framework에 비해 가벼움, 성능, 배포가 훨씬 용이하다.
학습 순서 및 접근 방법
주제 | 메인사이트 | 튜토리얼 |
---|---|---|
XAML 디자인 | WPF-Microsoft | ToskersCorner AngelSix Design com WPF |
Caliburn.Micro - MVVM | Caliburn.Micro | IAmTimCorey Dot NET Guide NET Interview Preparation |
Dapper - Simple Object Mapper | Dapper | IAmTimCorey CodAffection |
EPPlus - Excel spreadsheets | EPPlus | EVERYDAY BE CODING David Stovell |
FastReport Open Source - Reporting Tool |
FastReport OpenSource | Documentation fropensource Fast Reports |
ConfuserEx2, ILSpy - Source Protector |
ConfuserEx ILSpy |
Documentation ILSpy |
C# Tutorial | NET Core Home | freeCodeCamp kudvenkat |
Caliburn.Micro, MVVM에서 기본적인 프로젝트 구성 폴더(Commands, Helper, Models, ViewModels, View)와 라이브러리 개발 구성(Dapper, Repository, Database)d에 따른 소스를 부분적으로 작성하고 테스트했다. 아래는 해당 소스이다.
Caliburn.Micro , MVVM 템플릿
전체 소스는 GitHub에 올리기로 하고 아래의 내용은 프로그램 전체 소스의 주요 부분만 간추린 것이다. Visual Studio에서 기본적인 옵션으로 WPF 프로젝트를 생성한 후에 Nuget에서 Caliburn.Micro 패키지를 설치한다. 기본적으로 생성된 MainWindow.xaml(.cs)를 삭제하고 아래의 순서에 따라 기본 틀을 작성한다.
App.xaml 수정
<!-- StartupUri="MainWindow.xaml" 제거 후 리소스등록 -->
<Application x:Class="WPFUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WPFUI">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<local:Bootstrapper x:Key="Bootstrapper" />
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Bootstrapper 클래스 작성
//Root폴더에 작성
using Caliburn.Micro;
using System;
using System.Collections.Generic;
using System.Windows;
using WPFUI.Helper;
using WPFUI.ViewModels;
namespace WPFUI
{
public class Bootstrapper : BootstrapperBase
{
private readonly SimpleContainer simpleContainer = new SimpleContainer();
public Bootstrapper()
{
Initialize();
}
protected override void OnStartup(object sender, StartupEventArgs e)
{
// 메인 윈도우 등록
DisplayRootViewFor<ShellViewModel>();
}
protected override void Configure()
{
simpleContainer.Instance(simpleContainer);
simpleContainer.Singleton<IWindowManager, WindowManager>().Singleton<IEventAggregator, EventAggregator>();
// 이부분은 사용자 클래스(옵션)
simpleContainer.Singleton<IMessageProvider, MessageProvider>();
// 필요한 View(Window)를 싱글톤으로 등록
simpleContainer.Singleton<ShellViewModel>();
simpleContainer.Singleton<FirstViewModel>();
simpleContainer.Singleton<SecondViewModel>();
simpleContainer.Singleton<ThirdViewModel>();
}
protected override object GetInstance(Type service, string key)
{
return simpleContainer.GetInstance(service, key);
}
protected override IEnumerable<object> GetAllInstances(Type service)
{
return simpleContainer.GetAllInstances(service);
}
protected override void BuildUp(object instance)
{
simpleContainer.BuildUp(instance);
}
}
}
ShellViewModels.cs
// ViewModels 폴더에 작성
using Caliburn.Micro;
using WPFUI.Helper;
namespace WPFUI.ViewModels
{
public class ShellViewModel : Conductor<IScreen>.Collection.OneActive
{
private readonly IMessageProvider messageProvider;
private string mTextBlockItemCount;
public string TextBlockItemCount
{
get { return mTextBlockItemCount; }
set
{
mTextBlockItemCount = value;
NotifyOfPropertyChange(() => TextBlockItemCount);
}
}
public ShellViewModel(IMessageProvider messageProvider)
{
this.messageProvider = messageProvider;
}
public string TitleMessage => messageProvider.Get();
public void ButtonFirst()
{
ActiveItem = IoC.Get<FirstViewModel>();
}
public void ButtonSecond()
{
ActiveItem = IoC.Get<SecondViewModel>();
}
public void ButtonThird()
{
ActiveItem = IoC.Get<ThirdViewModel>();
}
public void ButtonItemCount()
{
TextBlockItemCount = Items.Count.ToString();
}
}
}
ShellView.xaml 윈도우
<!--
추가로 임의의 윈도우가 3개 있다고 가정...
btn1 : ButtonFirst <-> FirstViewModel : ButtonFirst
btn2 : ButtonSecond <-> SecondViewModel : ButtonSecond
btn3 : ButtonThired <-> ThirdViewModel : ButtonThired
Views 폴더에 작성
-->
<Window x:Class="WPFUI.Views.ShellView" xmlns="...중략...">
<!-- 중략 -->
<Grid Background="#005FBF">
<!-- 중략 -->
<!-- 뷰모델과 뷰 바인딩 -->
<StackPanel Grid.Column="1" Grid.Row="2" MinWidth="120" Background="AntiqueWhite">
<Button x:Name="ButtonFirst" Content="btn1" />
<Button x:Name="ButtonSecond" Content="btn2" />
<Button x:Name="ButtonThird" Content="btn3" />
</StackPanel>
<!-- 중략 -->
</Grid>
</Window>
참고 : Helper Class
/***함수: GetPropertyDisplayName, FindChild, RefreshControl
public string UserID => HelperUtils.GetPropertyDisplayName<FirstModel>(i => i.UserID);
Button btn = HelperUtils.FindChild<Button>(Application.Current.MainWindow, param.ToString());
btn.RefreshControl();
***/
namespace WPFUI.Helper
{
public static class HelperUtils
{
public static T FindChild<T>(DependencyObject parent, string childName) where T : DependencyObject
{
if (parent == null) return null;
T foundChild = null;
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
T childType = child as T;
if (childType == null)
{
foundChild = FindChild<T>(child, childName);
if (foundChild != null) break;
}
else if (!string.IsNullOrEmpty(childName))
{
var frameworkElement = child as FrameworkElement;
if (frameworkElement != null && frameworkElement.Name == childName)
{
foundChild = (T)child;
break;
}
}
else
{
foundChild = (T)child;
break;
}
}
return foundChild;
}
private static readonly Action EmptyDelegate = delegate { };
public static void RefreshControl(this UIElement uiElement)
{
uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
}
public static T GetAttribute<T>(this MemberInfo member, bool isRequired) where T : Attribute
{
var attribute = member.GetCustomAttributes(typeof(T), false).SingleOrDefault();
if (attribute == null && isRequired)
{
throw new ArgumentException(
string.Format(CultureInfo.InvariantCulture, "{0} 속성은 {1} 멤버에 정의되어야 합니다.", typeof(T).Name, member.Name));
}
return (T)attribute;
}
public static string GetPropertyDisplayName<T>(Expression<Func<T, object>> propertyExpression)
{
var memberInfo = GetPropertyInformation(propertyExpression.Body);
if (memberInfo == null)
{
throw new ArgumentException("속성 참조 표현식이 없습니다.", "propertyExpression");
}
var attr = memberInfo.GetAttribute<DisplayNameAttribute>(false);
if (attr == null)
{
return memberInfo.Name;
}
return attr.DisplayName;
}
public static MemberInfo GetPropertyInformation(System.Linq.Expressions.Expression propertyExpression)
{
Debug.Assert(propertyExpression != null, "propertyExpression != null");
MemberExpression memberExpr = propertyExpression as MemberExpression;
if (memberExpr == null)
{
if (propertyExpression is UnaryExpression unaryExpr && unaryExpr.NodeType == ExpressionType.Convert)
{
memberExpr = unaryExpr.Operand as MemberExpression;
}
}
if (memberExpr != null && memberExpr.Member.MemberType == MemberTypes.Property)
{
return memberExpr.Member;
}
return null;
}
}
}