C# - WPF, MVVM 노트 (1)

업데이트:

WPF 애플리케이션 생성

객체 생성

namespace BusinessLogic
{
    public class Human
    {
        public string FirstName { get; set; }
        public bool HasDrivingLicense { get; set; }
    }

    public class Car
    {
        public double Speed { get; set; }
        public Color color { get; set; } 
        public Human Driver { get; set; }
    }
}


C#에서 생성

using BusinessLogic;

var h = new Human();
h.FirsName = "Nick";
h.HasDrivingLicense = true;

var c = new Car();
c.Color = Colors.Red;
c.Driver = h;


XAML에서 생성

<Label xmlns:bl="clr-namespace:BusinessLogic">
    <bl:Car Color="Red">
        <b1:Car.Driver>
            <bl:Human FirstName="Nick" HasDrivingLicense="true" />
        </bl:Car.Driver>
    </bl:Car>
</Label>


명명 규칙

XAML 객체를 코드 비하인드나 다른 XAML에서 사용할 수 있다.

<Label xmlns:bl="clr-namespace:BusinessLogic">
    <bl:Car x:Name="myCar" Speed="100" Color="Red" />
</Label>
myCar.Color = Color.Blue;
  • x:Name: 항상 사용 가능
  • Name: WPF 컨트롤에서만 사용 가능


이벤트

WPF 컨트롤은 이벤트를 선언하며 XAML의 특성으로 사용 가능하다.

<Button Click="Greet">


private void Greet (object sender, RoutedEventArgs e)
{
    MessageBox.Show("");
}

<br>

이벤트는 컨트롤 트리를 대부분 아래로 이동한다.

```xml
<Grid MouseLeftButtonDown="SaySomething">
    <Button MouseLeftButtonDown="SayHello" />
    <Button MouseLeftButtonDown="SayGoodbye" />
</Grid>


크기 할당

Grid 컨트롤은 자식의 크기를 제한하지만 Canvas 컨트롤은 자식 컨트롤의 크기를 제한하지 않는다.

<Grid Width="50" Height="50" Background="Orange">
    <Button Content="Hello world" Margin="5">
</Grid>
<Canvas Width="50" Height="50" Background="Orange">
    <Button Content="Hello world" Margin="5">
</Canvas>


Panel 컨트롤

  • 하나의 컨트롤만 허용하는 여러 개의 컨트롤을 표시한다.
  • 사용 가능한 크기에 따라 컨트롤을 배치한다.

Canvas

Canvas 패널을 사용하면 자체 좌표를 제공하는 컨르롤을 배치할 수 있다.
어떠한 크기도 강요하지 않고 자유로운 배치가 가능하다.

<Canvas>
    <Button Canvas.Top="0" Canvas.Left="0">A</Button>
    <Button Canvas.Top="25" Canvas.Left="0">B</Button>
    <Button Canvas.Top="25" Canvas.Left="25">C</Button>
    <Button Canvas.Top="0" Canvas.Left="50">D</Button>
</Canvas>


StackPanel

스택처럼 원하는 방향으로 자신 컨트롤을 쌓을 수 있다.


DockPanel

DockPanel에 컨트롤을 배치하면 데스크탑 애플리케이션과 같은 화면 레이아웃을 빠르게 얻을 수 있다.

<DockPanel>
    <Button DockPanel.Dock="Left">
        Left
    </Button>
    <Button DockPanel.Dock="Right">
        Right
    </Button>
</DockPanel>


WrapPanel

XAML을 수동으로 편집할 때 사용하고 쉽다. 안드로이드에 Wrap 속성과 비슷


UniformGrid

xaml을 수동으로 편집할 때, 시간을 들이지 않고 UI를 배치할 수 있다.

<UniformGrid>
    <Label>Name</Label>
    <TextBox Width="70">
    <Label>Age</Label>
    <ComboBox />
</UniformGrid>


Grid

Grid컨트롤은 최상위 다기능 컨트롤이다.

다음과 같이 ColumnDefinitions, RowDefinitions 속성을 통해 행과 열을 정의할 수 있다.

<Grid Width="200" Height="100">
    <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>
    <Button Grid.Row="0" Grid.Column="0">A</Button>
    <Button Grid.Row="1" Grid.Column="0">B</Button>
    <Button Grid.Row="1" Grid.Column="1">C</Button>
    <Button Grid.Row="0" Grid.Column="1">D</Button>
</Grid>

image


<Grid.ColumnDefinitions>
    <ColumnDefinition Width="30" />
    <ColumnDefinition Width="*" /> // 남은 너비/높이에 비례한 크기로 결정
    <ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>


Panel 요약

  • Canvas: 강제: X, 디자인 뷰
  • DockPanel: 강제: O, XAML
  • Grid: 강제: O, 디자인 뷰
  • StackPanel: 강제: O, XAML
  • UniformGrid: 강제: O, XAML
  • WrapPanel: 강제: O, XAML


목록 컨트롤

목록 컨트롤은 컨트롤 모양을 변경하는 템플릿과 항목의 표시 방법을 변경하는 항목 템플릿을 제공한다.

<ListBox Height="100">
    <Label>Element 1</Label>
    <Label>Element 2</Label>
    <GroupBox Header="Element 3">
        This?
    </GroupBox>
</ListBox>


<ComboBox Height="100">
    <Label>Element 1</Label>
    <Label>Element 2</Label>
    <GroupBox Header="Element 3">
        This?
    </GroupBox>
</ComboBox>

TabControl은 헤더와 내용을 가질 수 있다. 하위 컨트롤로 TabItem 요소를 사용해 구체화한다.


페이지 변경

 private void Button_Click(object sender, RoutedEventArgs e)
{
    NavigationService.Navigate(new Uri("/Contact.xaml", UriKind.Relative));
}

열, 행 병합

Grid.ColumnSpan
Grid.RowSpan


데이터 관리

데이터 바인딩

<TextBox Text="{Binding Speed}" />
<TextBox Text="{Binding Path=Speed}" />
<TextBox Text="{Binding Speed, ElementName=c}" />

<TextBox Text="{Binding Source={StaticResource someList}, Path=Height}"> <!-- someList 컨트롤 높이가 TextBox 실제 높이로 표시된다. -->


예제

<StackPanel>
    <Slider Maximum="100"
        Value="10"
        x:Name="slider" />
    <ProgressBar Value="{Binding Value, ElementName=slider}"></ProgressBar>
    <TextBox Text="{Binding Value, ElementName=slider}"></TextBox>          
</StackPanel>
<Window
    Background="{Binding Text, ElementName=color}">
    <TextBox Text="Yellow"
        x:Name="color" />
</Window>


바인딩 모드

주로 TwoWay와 OneWay를 사용한다.

  • TwoWay: 대상 변경: Y, 값 변경: Y
  • OneWay: 대상 변경: X, 값 변경: Y
  • OneWayToSource: 대상 변경: Y, 값 변경: X
  • OneTime: 대상 변경: X, 값 변경: X
{Binding Path=Speed, Mode=TwoWay}


바인딩 오류

데이터 바인딩은 잘못된 오류임에도 불구하고 오류로 감지하지않고 그냥 무시하는 경우가 많다.

  • 디버그 모드로 실행
  • 수동으로 화면 이동
  • 디버그 출력 창에서 System.Data.Error 줄을 확인


DataContext

실제로 다음과 같이 DataContext 할당되어 동작하게 된다.

<StackPanel DataContext="...">
    <TextBox Text="{Binding Name}" />
    <Label Content="{Binding SSN}" />
</StackPanel>

이렇게 할 수도 있고

this.DataContext = ... ;

이런식으로 코드 비하인드에 할당할 수도 있다.

명시적으로 DataContext를 할당하지 않는다면 null을 기본 값으로, 부모 컨트롤의 DataContext가 사용된다.


변환기

앞 전에서 문자열을 double이나 브러쉬로 변환했었다. 그것은 변환 시스템을 자동으로 사용하여 이루어지는데 이를 확장할 수 있다.

변환기는 IValueConverter 인터페이스를 구현하여 사용한다. 양방향 바인딩에는 ConverterBack, 표준은 Convert 메소드를 구현한다.

xaml에서는 구현한 변환기 클래스의 인스턴스를 참조하여 사용한다.

namespace Maths {
    public class TwiceConverter : IValueConverter {
        public object Convert(object value,
            Type targetType, object parameter,
            CultureInfo culture)
        {
            return ((int)value)*2;
        }

        // 여기에 빈 ConvertBack 메소드가 들어간다
    }
}


<Page xmlns:c="clr-namespace:Maths">
    <Page.Resources>
        <c:TwiceConverter x:Key="twiceConv" />
    </Page.Resources>
    <TextBlock Value="{Binding Speed, Converter={Static\
    Resource twiceConv}}">
</Page>


목록 컨트롤에서의 컬렉션

한번에 여러 개의 속성을 보여줘야하는 경우도 있다.

목록 컨트롤의 경우는 IEnumerable 인터페이스를 구현하고있고 ItemSource 속성이 있는데 해당 속성에 컬렉션을 지정하면 모든 컬렉션 요소가 표시 된다.

var cars = new List<Car>();
for (int i = 0; i < 10; i++)
{
    cars.Add(new Car() {
        Speed = i * 10
    });
}
this.DataContext = cars
<ListBox ItemsSource="{Binding}">

DataContext를 사용하여 굉장히 간단하게 되었다.

자동차 컬렉션이 DataContext에 할당되어 있으므로 경로 없이 ItemSource 속성에 바인딩하거나 소스를 그냥 자동차 컬렉션에 연결하면 된다.


목록 컨트롤 사용자 정의

이렇게하면 실제로 BusinessLogic.Car 으로 출력이 된다.
왜냐하면 WPF가 Car 클래스 인스턴스를 표시하는 방법을 몰라서 각 인스턴스의 ToString 메소드를 호출하기 때문이다.

모든 목록 컨트롤에는 항목을 표시하는 방법을 사용자 정의할 수 있는 속성이 있다.

  • ItemsPanel: 요소 배치 방법 설명 (항목 레이아웃)
  • ItemTemplate: 각 요소에 대해 반복이 필요한 템플릿 제공 (각 항목의 모양)
  • ItemContainerStyle: 항목을 선택하거나 마우스를 올릴 때 동작 방법을 설명 (요소 효과)
  • Template: 컨트롤 자체를 렌더링하는 방법을 설명 (목록 주위 ex. 테두리, 배경, 스크롤바, …)


ItemTemplate 속성은 각 목록 항목에 대해 반복되는 DataTemplate이어야 한다.
DataTemplate 내부 요소는 데이터 바인딩 식을 사용하여 해당 속성을 기본 항목 속성에 연결할 수 있다.

실제로 DataTemplate의 DataContext는 표시되는 항목이다.

<ListBox ItemsSource="{Binding}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Speed}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>


<ListBox ItemsSource="{Binding}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding Speed}" />
                <Slider Value="{Binding Speed}" />
                <TextBlock Text="{Binding Color}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>


INotifyPropertyChanged

컨트롤을 통해 속성을 업데이트하면 바인딩된 다른 컨트롤이 없데이트 되지만, 이벤트나 웹 데이터 등의 코드 자체로 인해 속성이 변경되면 바인딩된 컨트롤이 없데이트 되지 않는다.

따라서 속성이 변경되면 이벤트를 발생시켜 주어야하는데 **INotifyPropertyChanged** 인터페이스를 구현하면 된다.

using System.ComponentModel;

public class Notifier : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        if(PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}


이제 Notifier 클래스를 상속하고 Setter에서 위에서 재정의한 OnPropertyChanged() 함수를 호출하면 업데이트가 된다.

public class Car: Notifier
{
    private double speed;

    public double Speed
    {
        get { return speed; }
        set
        {
            speed = Value;
            OnPropertyChanged("Speed");
        }
    }
}


INotifyCollectionChanged

방금 설명했던 INotifyPropertyChanged 인퍼테이스만 사용하면 메시지를 주고 받을 때 모든 내용을 제거하고 다시 추가해야 한다. 현재 스크롤로 인해 보이지 않는 경우 특히 시간이 많이 소모된다.

**INotifyCollectionChanged**를 사용하면 컬렉션에서 추가, 제거, 변경을 알릴 수 있다.

실제로 INotifyCollectionChanged 인터페이스인 Observable Collection를 구현한다는 점을 제외하곤 List와 같은 클래스를 제공한다. 이를 사용하여 더욱 세밀한 UI 업데이트를 할 수 있다.


태그: , ,

카테고리:

업데이트:

댓글남기기