WPF-UI 튜토리얼 #4

guru chun (haechul chun)·2025년 2월 4일
0

WPF 개발

목록 보기
5/10

Pages

MainWindow에는 메뉴가 있고, 메뉴를 선택하면 컨텐츠 영역에 메뉴에 해당하는 페이지가 표시된다. 현재 프로젝트는 3개의 페이지를 샘플로 제공하고 있다.

  • DashboardPage: 버튼과 카운터
  • DataPage: 배열(컬렉션)을 바인딩, View-ViewModel-Model의 사용
  • SettingsPage: 라디오 버튼, 테마 변경
    MainWindow는 INavigationWindow를 구현하고, Page는 INavigableView를 구현한다. 이 차이로 HostService가 MainWindow와 Page를 구분할 수 있는 것으로 보인다.
    여기서는 View와 ViewModel이 어떻게 구현되었는지 코드를 살펴본다. 이미 MainWindow의 코드를 살펴보았기 때문에 Page의 코드는 아주 쉽게 이해할 수 있다.

DashboardPage

이 페이지는 ViewModel의 값을 View에 어떻게 표시하는지, View의 버튼을 눌렀을 때 ViewModel에서 버튼 클릭 이벤트를 어떻게 처리하는지 예시를 보여준다.

Dashboardpage.xaml


MainWindow의 Title 영역과 NavigationView 영역은 앞서 MainWindow에서 살펴보았다.
DashboardPage는 하나의 버튼과 TextBlock을 가진다.

<Grid VerticalAlignment="Top">
	<Grid.ColumnDefinitions>
		<ColumnDefinition Width="Auto" />
		<ColumnDefinition Width="Auto" />
	</Grid.ColumnDefinitions>
	<ui:Button
		Grid.Column="0"
		Command="{Binding ViewModel.CounterIncrementCommand, Mode=OneWay}"
		Content="Click me!"/>
	<TextBlock
		Grid.Column="1"
		Text="{Binding ViewModel.Counter, Mode=OneWay}" />
</Grid>
  • 2개의 컨트롤 나란히 놓기 위해 Grid Layout에 2개의 컬럼을 정의한다.
  • Button은 첫번째 컬럼에 두고, Command 속성에 ViewModel.CounterIncrementCommand를 바인딩한다.
  • TextBlock은 두번째 컬럼에 두고, Text 속성을 ViewModel.Counter에 바인딩한다.

몇가지 궁금한 점이 생긴다.

  • View는 Button과 TextBlock이 Binding하는 ViewModel이 무엇인지 어떻게 알까?
  • WinForm이라면 Button의 이벤트 핸들러로 동작하는데, ViewModel의 Command는 이벤트핸들러일까?
  • Binding할 때 지정하는 Mode=OneWay는 어떤 의미인가?

DashboardPage(View)와 DataboadViewModel의 연결

View에서 참조하는 ViewModel은 어디서 왔는가?

public partial class DashboardPage : INavigableView<DashboardViewModel>
{
	public DashboardViewModel ViewModel { get; }
	public DashboardPage(DashboardViewModel viewModel)
	{
		ViewModel = viewModel;
		...
	}
}
  • View가 생성될 때 ViewModel을 인자로 받는다.
  • 그렇다면 View를 누가 생성하고 어떻게 이 View에 적합한 ViewModel을 찾아서 넘겨줄까?
    • xaml 파일에서 View의 DataContext에 ViewModel을 지정해주면 명확한데, 위에서 확인한 DashboardPage의 xaml에는 이런 코드가 보이지 않는다.
    • 코드의 어디선가 View와 ViewModel을 매칭해주어야 한다.
    • class DashboardPage : INavigableView<DashboardViewModel> 이 코드를 보면 클래스를 정의할 때 제너릭 인터페이스인 INavigableView<T>를 사용하고, 이 때 ViewModel을 Type으로 사용한다. 이 부분은 나중에 더 살펴봐야할 것 같다.

DashboardViewModel을 살펴보자.

public partial class DashboardViewModel : ObservableObject
{
	[ObservableProperty]
	private int _counter = 0;

	[RelayCommand]
	private void OnCounterIncrement()
	{
		Counter++;
	}
}
  • DashboardViewModel은 ObservableObject를 상속하고 있다.
  • View와 ViewModel이 어떻게 Binding되는지, 값이 변경되었을 때 컨트롤이 어떻게 새로운 값으로 업데이트되는지 이해하려면 ObservableObject의 동작을 알아야 한다. 자세한 내용은 나중에 알아보기로 ObservableObject는 값 변경을 Notify 할 수 있다는 정도만 기억하고 넘어가자.
  • Member Data와 Method에 [xxx] 형태의 Annotation 같은 것이 보인다.
    • 이것은 패턴화된 코드를 컴파일러가 자동으로 생성하도록 한다.
    • _counter 앞에 [ObservableProperty]는 Counter라는 Property를 만들고, 이 Property가 변경되면 ObservableObject의 SetProperty()를 자동으로 호출한다는 의미이다.
      • 이 Method를 호출하면 Object의 Observer들에게 값변경을 Notify한다.
      • View의 컨트롤에서 ViewModel의 Property를 Binding하면 컨트롤이 Observer로 등록되는 것 같다.
    • OnCounterIncrement() 앞에 [RelayCommand]는 RelayCommand 객체가 생성되고 이 함수가 RelayCommand의 핸들러로 사용된다는 것을 의미한다.
    • 이 Annodation에 의해 어떤 코드가 생성되는지는 나중에 더 알아보기로 하고 이 정도로 넘어가자.

다시 View로 돌아가 보자.

  • View와 ViewModel이 어떻게 연결되는지 아직은 명확하지 않지만 WPF-UI 내부에서 이루어지는 것은 짐작해 볼 수 있다.
  • Button에 Binding되는 ViewModel의 CounterIncrementCommand[RelayCommand] Annotation에 의해 생성되는 RelayCommand 객체이다.
  • WPF에서 BindingMode는 다음과 같다.
    • TwoWay : 양방향 바인딩. 변경되는 쪽을 기준으로 반대 쪽을 업데이트 해준다.
    • OneWay : 소스 속성(ViewModel)이 변경될 때만 대상 속성(View)을 업데이트한다.
    • OneTime : 응용 프로그램이 시작되거나 DataContext가 변경되는 경우에 1회만 대상 속성을 업데이트 한다.
    • OneWayToSource : 대상 속성(View)이 변경될 때 소스 속성(ViewModel)을 업데이트 한다.(OneWay와 반대로 동작)
    • Default : 대상 속성(View)에 지정된 바인딩 모드 기본 값을 사용한다.
  • 위 예시(xaml)에서 Button과 TextBlock은 OneWay로 바인딩 된다.
    • TextBlock의 값은 ViewModel의 Counter값이 바뀔때마다 업데이트된다.
    • 그런데 버튼은 View에서 ViewModel로 Command를 보내는 것이 목적인데 왜 OneWay로 바인딩될까? ViewModel에서 Command를 처리한 결과에 따라 버튼의 상태가 바뀌기라도 하는 것일까?

DataPage

이 페이지는 ViewModel의 컬렉션(배열, 리스트) 데이터를 View에 어떻게 표시하는지 예시를 보여준다.

DataPage.xaml

<Grid>
	<ui:VirtualizingItemsControl
		Foreground="{DynamicResource TextFillColorSecondaryBrush}"
		ItemsSource="{Binding ViewModel.Colors, Mode=OneWay}"
		VirtualizingPanel.CacheLengthUnit="Item">
		<ItemsControl.ItemTemplate>
			<DataTemplate DataType="{x:Type models:DataColor}">
				<ui:Button
					...
					Background="{Binding Color, Mode=OneWay}"
				/>
			</DataTemplate>
		</ItemsControl.ItemTemplate>
	</ui:VirtualizingItemsControl>
</Grid>
  • VirtualizingItemsControl은 여러 개의 Control을 담을 수 있는 일종의 Flowlayout 패널이다.
  • VirtualizingItemsControl에 ViewModel의 Colors 컬렉션을 바인딩한다.
  • 컬렉션의 각 항목을 ItemsControl에 어떻게 표시할지(예, 버튼과 값) ItemTemplate을 정의한다. 이 Template은 항목에 컬렉션의 값(Model)을 바인딩하는 DataTemplate을 설정한다.

DataViewModel, DataColor

DataViewModel(ViewModel)은 DataPage(View)에 제공할 데이터(컬렉션)을 가진다.

public partial class DataViewModel : ObservableObject, INavigationAware
{
	...
	[ObservableProperty]
	private IEnumerable<DataColor> _colors;
	...
}
  • ViewModel에서 제공하는 데이터(컬렉션)은 Colors라는 속성이름으로 접근 가능하다.
  • [ObservableProperty]에 의해 ViewModel에 Colors 속성이 자동으로 정의된다.
  • 이 속성의 Type은 DataColor(Model)이고 다음과 같이 정의된다.
public struct DataColor
{
    public Brush Color { get; set; }
}

Model의 속성 바인딩

ItemsControl에 ItemTemplate이 정의되고 각 항목에 Color(Model) 값을 지정하기 위해서 DataTemplate을 정의한다.

xmlns:models="clr-namespace:UiDesktopApp1.Models"
...
<DataTemplate DataType="{x:Type models:DataColor}">
	<ui:Button
		...
		Background="{Binding Color, Mode=OneWay}"
	/>
</DataTemplate>
  • models:DataColor ViewModel에서 넘겨 받은 데이터의 Type이 Models.DataColor 클래스임을 알려준다.
  • 각 항목을 표시하는데 Button을 사용한다. (여러개의 컨트롤을 매핑해도 된다)
  • Button의 BackgroundColor를 데이터(Type=Model.DataColor)의 Color 속성에 바인딩한다.

SettingsPage

이 페이지는 컨트롤의 속성 Type과 ViewModel에서 제공하는 데이터의 Type이 불일치하는 경우 Converter를 통해 변환해서 Binding하는 모습을 보여주고, Command를 ViewModel에 보낼 때 Parameter를 지정하는 예시도 확인할 수 있다.

SettingsPage.xaml

화면에는 테마(Light/Dark)를 선택할 수 있는 Radio 버튼이 있다. 이 버튼을 선택하면 App.xaml에서 DynamicResouce로 참조하고 있는 WPF-UI Theme를 즉시 변경한다.

<StackPanel>
	...
	<RadioButton
		Command="{Binding ViewModel.ChangeThemeCommand, Mode=OneWay}"
		CommandParameter="theme_light"
		Content="Light"
		GroupName="themeSelect"
		IsChecked="{Binding ViewModel.CurrentTheme, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter=Light, Mode=OneWay}" />
	<RadioButton
		Command="{Binding ViewModel.ChangeThemeCommand, Mode=OneWay}"
		CommandParameter="theme_dark"
        .../>
	...
</StackPanel>
  • 2개의 라디오버튼이 모두 Command는 ViewModel.ChangeThemeCommand에, IsChecked는 ViewModel.CurrentTheme에 바인딩된다.
  • 라디오버튼을 선택하면 ViewModel의 Command가 실행되면서 Application의 테마를 변경한다.
  • 현재 선택된 테마(ViewModel.CurrentTheme)에 따라서 각 라디오버튼의 상태를 업데이트한다.

SettingsViewModel

[ObservableProperty]로 AppVersion, CurrentTheme 속성을 제공한다.
[RelayCommand]로 ChangeThemeCommand 커맨드를 제공한다.


이상으로 WPF-UI의 Application Wizard로 생성한 MVVM 예제소스를 살펴 보았다. 앞에서 자세히 설명하지 못한 몇가지 개념과 관련 클래스를 설명하는 것으로 튜토리얼을 마무리하겠다.

profile
오늘도, 내일도 코딩을 즐기자

0개의 댓글