다음을 통해 공유


데이터 바인딩 및 MVVM

Browse sample. 샘플 찾아보기

MVVM(Model-View-ViewModel) 패턴은 보기라고 하는 XAML 사용자 인터페이스, 모델이라고 하는 기본 데이터 및 viewmodel이라고 하는 뷰와 모델 간의 중개자 등 세 가지 소프트웨어 계층을 분리합니다. 뷰 및 viewmodel은 종종 XAML에 정의된 데이터 바인딩을 통해 연결됩니다. BindingContext 뷰의 경우는 일반적으로 viewmodel의 인스턴스입니다.

Important

.NET 다중 플랫폼 앱 UI(.NET MAUI)는 바인딩 업데이트를 UI 스레드에 마샬링합니다. MVVM을 사용하는 경우 .NET MAUI의 바인딩 엔진을 사용하여 모든 스레드에서 데이터 바인딩 viewmodel 속성을 업데이트하여 UI 스레드에 대한 업데이트를 가져올 수 있습니다.

MVVM 패턴을 구현하는 방법에는 여러 가지가 있으며 이 문서에서는 간단한 접근 방식에 중점을 둡니다. 뷰 및 viewmodel을 사용하지만 모델은 사용하지 않고 두 계층 간의 데이터 바인딩에 초점을 맞춥니다. .NET MAUI에서 MVVM 패턴을 사용하는 방법에 대한 자세한 설명은 .NET MAUI를 사용하는 엔터프라이즈 애플리케이션 패턴의 MVVM(Model-View-ViewModel)을 참조하세요. MVVM 패턴을 구현하는 데 도움이 되는 자습서는 MVVM 개념으로 앱 업그레이드를 참조 하세요.

단순 MVVM

XAML 태그 확장에서는 XAML 파일이 다른 어셈블리의 클래스를 참조할 수 있도록 새 XML 네임스페이스 선언을 정의하는 방법을 알아보았습니다. 다음 예제에서는 태그 확장을 사용하여 네임스페이 x:Static 스의 정적 DateTime.Now 속성 System 에서 현재 날짜 및 시간을 가져옵니다.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:sys="clr-namespace:System;assembly=netstandard"
             x:Class="XamlSamples.OneShotDateTimePage"
             Title="One-Shot DateTime Page">

    <VerticalStackLayout BindingContext="{x:Static sys:DateTime.Now}"
                         Spacing="25" Padding="30,0"
                         VerticalOptions="Center" HorizontalOptions="Center">

        <Label Text="{Binding Year, StringFormat='The year is {0}'}" />
        <Label Text="{Binding StringFormat='The month is {0:MMMM}'}" />
        <Label Text="{Binding Day, StringFormat='The day is {0}'}" />
        <Label Text="{Binding StringFormat='The time is {0:T}'}" />

    </VerticalStackLayout>

</ContentPage>

이 예제에서는 검색된 DateTime 값이 에 StackLayout있는 BindingContext 값으로 설정됩니다. 요소를 설정 BindingContext 하면 해당 요소의 모든 자식에 의해 상속됩니다. 즉, 모든 자식이 StackLayout 동일 BindingContext하며 해당 개체의 속성에 대한 바인딩을 포함할 수 있습니다.

Screenshot of a page displaying the date and time.

그러나 문제는 페이지가 생성 및 초기화될 때 날짜와 시간이 한 번 설정되고 변경되지 않는다는 것입니다.

XAML 페이지는 항상 현재 시간을 표시하는 클록을 표시할 수 있지만 추가 코드가 필요합니다. MVVM 패턴은 시각적 개체와 기본 데이터 간의 속성에서 데이터를 바인딩할 때 .NET MAUI 앱에 대한 자연스러운 선택입니다. MVVM을 고려할 때 모델과 viewmodel은 코드로 완전히 작성된 클래스입니다. 뷰는 데이터 바인딩을 통해 viewmodel에 정의된 속성을 참조하는 XAML 파일인 경우가 많습니다. MVVM에서 모델은 viewmodel을 무시하고 viewmodel은 뷰를 무시합니다. 그러나 viewmodel에서 노출하는 형식을 UI와 연결된 형식에 맞게 조정하는 경우가 많습니다.

참고 항목

여기에 표시된 것과 같은 MVVM의 간단한 예제에서는 모델이 전혀 없는 경우가 많으며 패턴에는 데이터 바인딩과 연결된 뷰 및 viewmodel만 포함됩니다.

다음 예제에서는 1초마다 업데이트되는 단일 DateTime 속성이 있는 시계에 대한 viewmodel을 보여줍니다.

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace XamlSamples;

class ClockViewModel: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private DateTime _dateTime;
    private Timer _timer;

    public DateTime DateTime
    {
        get => _dateTime;
        set
        {
            if (_dateTime != value)
            {
                _dateTime = value;
                OnPropertyChanged(); // reports this property
            }
        }
    }

    public ClockViewModel()
    {
        this.DateTime = DateTime.Now;

        // Update the DateTime property every second.
        _timer = new Timer(new TimerCallback((s) => this.DateTime = DateTime.Now),
                           null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
    }

    ~ClockViewModel() =>
        _timer.Dispose();

    public void OnPropertyChanged([CallerMemberName] string name = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

Viewmodels는 일반적으로 해당 속성 중 하나가 변경 될 때마다 이벤트를 발생 PropertyChanged 시키는 클래스에 대 한 기능을 제공 하는 인터페이스를 구현 INotifyPropertyChanged 합니다. .NET MAUI의 데이터 바인딩 메커니즘은 이 PropertyChanged 이벤트에 처리기를 연결하므로 속성이 변경될 때 알림을 받고 대상을 새 값으로 업데이트된 상태로 유지할 수 있습니다. 이전 코드 예제에서 메서드는 OnPropertyChanged 속성 원본 이름을 DateTime자동으로 확인하면서 이벤트 발생을 처리합니다.

다음 예제에서는 사용하는 XAML을 보여 줍니다.ClockViewModel

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.ClockPage"
             Title="Clock Page">
    <ContentPage.BindingContext>
        <local:ClockViewModel />
    </ContentPage.BindingContext>

    <Label Text="{Binding DateTime, StringFormat='{0:T}'}"
           FontSize="18"
           HorizontalOptions="Center"
           VerticalOptions="Center" />
</ContentPage>

이 예제에서는 ClockViewModel using 속성 요소 태그로 ContentPage 설정 BindingContext 됩니다. 또는 코드 숨김 파일이 viewmodel을 인스턴스화할 수 있습니다.

Binding 속성 형식의 Text 속성에 Label 대한 태그 확장입니다DateTime. 다음 스크린샷은 결과를 보여줍니다.

Screenshot of a page displaying the date and time via a viewmodel.

또한 속성을 마침표로 구분하여 viewmodel 속성의 개별 DateTime 속성에 액세스할 수 있습니다.

<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >

대화형 MVVM

MVVM은 기본 데이터 모델을 기반으로 하는 대화형 보기에 양방향 데이터 바인딩과 함께 사용되는 경우가 많습니다.

다음 예제에서는 값을 , Saturation및 값으로 Hue변환하고 Luminosity 다시 다시 변환 Color 하는 방법을 보여 HslViewModel 줍니다.

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace XamlSamples;

class HslViewModel: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private float _hue, _saturation, _luminosity;
    private Color _color;

    public float Hue
    {
        get => _hue;
        set
        {
            if (_hue != value)
                Color = Color.FromHsla(value, _saturation, _luminosity);
        }
    }

    public float Saturation
    {
        get => _saturation;
        set
        {
            if (_saturation != value)
                Color = Color.FromHsla(_hue, value, _luminosity);
        }
    }

    public float Luminosity
    {
        get => _luminosity;
        set
        {
            if (_luminosity != value)
                Color = Color.FromHsla(_hue, _saturation, value);
        }
    }

    public Color Color
    {
        get => _color;
        set
        {
            if (_color != value)
            {
                _color = value;
                _hue = _color.GetHue();
                _saturation = _color.GetSaturation();
                _luminosity = _color.GetLuminosity();

                OnPropertyChanged("Hue");
                OnPropertyChanged("Saturation");
                OnPropertyChanged("Luminosity");
                OnPropertyChanged(); // reports this property
            }
        }
    }

    public void OnPropertyChanged([CallerMemberName] string name = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

이 예제에서는 , Saturation및 속성을 변경Hue하면 속성이 Color 변경되고 속성이 Color 변경되면 다른 세 가지 속성 Luminosity 이 변경됩니다. 속성이 변경되지 않는 한 viewmodel이 이벤트를 호출 PropertyChanged 하지 않는다는 점을 제외하면 무한 루프처럼 보일 수 있습니다.

다음 XAML 예제에는 해당 Color 속성이 viewmodel의 속성에 Color 바인딩되고 , 및 속성에 HueSaturation바인딩된 3 Slider 개와 Luminosity 3개의 Label 뷰가 포함 BoxView 됩니다.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.HslColorScrollPage"
             Title="HSL Color Scroll Page">
    <ContentPage.BindingContext>
        <local:HslViewModel Color="Aqua" />
    </ContentPage.BindingContext>

    <VerticalStackLayout Padding="10, 0, 10, 30">
        <BoxView Color="{Binding Color}"
                 HeightRequest="100"
                 WidthRequest="100"
                 HorizontalOptions="Center" />
        <Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
               HorizontalOptions="Center" />
        <Slider Value="{Binding Hue}"
                Margin="20,0,20,0" />
        <Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
               HorizontalOptions="Center" />
        <Slider Value="{Binding Saturation}"
                Margin="20,0,20,0" />
        <Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
               HorizontalOptions="Center" />
        <Slider Value="{Binding Luminosity}"
                Margin="20,0,20,0" />
    </VerticalStackLayout>
</ContentPage>

각각 Label 에 대한 바인딩은 기본값 OneWay입니다. 값을 표시하기만 하면 됩니다. 그러나 각각 Slider 에 대한 기본 바인딩은 .입니다 TwoWay. 이를 통해 Slider viewmodel에서 초기화할 수 있습니다. viewmodel이 인스턴스화 Color 되면 속성이 .로 Aqua설정됩니다. viewmodel의 Slider 속성에 대한 새 값을 설정한 다음 새 색을 계산하는 변경 내용입니다.

MVVM using two-way data bindings.

명령

경우에 따라 앱은 사용자가 viewmodel의 항목에 영향을 주는 명령을 시작하도록 요구하여 속성 바인딩을 넘어서야 합니다. 이러한 명령은 일반적으로 단추를 클릭하거나 손가락으로 탭하여 신호를 받으며, 대개 ButtonClicked 이벤트 또는 TapGestureRecognizerTapped 이벤트에 대한 처리기의 코드 숨김 파일에서 처리됩니다.

명령 인터페이스는 MVVM 아키텍처에 훨씬 더 적합한 명령을 구현하는 또 다른 방법을 제공합니다. viewmodel에는 클릭과 같은 뷰의 특정 활동에 대한 반응으로 실행되는 메서드인 명령이 Button 포함될 수 있습니다. 데이터 바인딩은 이러한 명령과 Button 간에 정의됩니다.

viewmodel과 viewmodel ButtonButton 간의 데이터 바인딩을 허용하려면 다음 두 가지 속성을 정의합니다.

참고 항목

다른 많은 컨트롤도 정의하고 속성을 정의 Command 합니다 CommandParameter .

인터페이스는 ICommand System.Windows.Input 네임스페이스에 정의되며 두 가지 메서드와 하나의 이벤트로 구성됩니다.

  • void Execute(object arg)
  • bool CanExecute(object arg)
  • event EventHandler CanExecuteChanged

viewmodel은 형식 ICommand의 속성을 정의할 수 있습니다. 그런 다음 이러한 속성을 CommandButton 요소 또는 다른 요소의 속성 또는 이 인터페이스를 구현하는 사용자 지정 뷰에 바인딩할 수 있습니다. 필요에 따라 이 viewmodel 속성에 CommandParameter 바인딩된 개별 Button 개체(또는 다른 요소)를 식별하도록 속성을 설정할 수 있습니다. 내부적으로 Button 사용자가 메서드를 Execute 탭할 때마다 메서드를 Button호출하여 메서드CommandParameterExecute 전달합니다.

CanExecute 메서드와 CanExecuteChanged 이벤트는 탭이 현재 유효하지 않을 수 있는 Button 경우에 사용되며, 이 경우 Button 자체적으로 사용하지 않도록 설정해야 합니다. Button 속성이 Command 처음 설정되고 이벤트가 발생할 때마다 CanExecuteChanged 호출 CanExecute 됩니다. 반환 falseButton 되는 경우 CanExecute 자체를 사용하지 않도록 설정하고 호출을 생성 Execute 하지 않습니다.

.NET MAUI에 Command 포함된 클래스 또는 Command<T> 클래스를 사용하여 인터페이스를 ICommand 구현할 수 있습니다. 이러한 두 클래스는 여러 생성자와 ChangeCanExecute viewmodel이 개체가 강제로 이벤트를 발생하도록 호출할 Command 수 있는 메서드를 CanExecuteChanged 정의합니다.

다음 예제에서는 전화 번호를 입력하기 위한 간단한 키패드의 viewmodel을 보여 줍니다.

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace XamlSamples;

class KeypadViewModel: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _inputString = "";
    private string _displayText = "";
    private char[] _specialChars = { '*', '#' };

    public ICommand AddCharCommand { get; private set; }
    public ICommand DeleteCharCommand { get; private set; }

    public string InputString
    {
        get => _inputString;
        private set
        {
            if (_inputString != value)
            {
                _inputString = value;
                OnPropertyChanged();
                DisplayText = FormatText(_inputString);

                // Perhaps the delete button must be enabled/disabled.
                ((Command)DeleteCharCommand).ChangeCanExecute();
            }
        }
    }

    public string DisplayText
    {
        get => _displayText;
        private set
        {
            if (_displayText != value)
            {
                _displayText = value;
                OnPropertyChanged();
            }
        }
    }

    public KeypadViewModel()
    {
        // Command to add the key to the input string
        AddCharCommand = new Command<string>((key) => InputString += key);

        // Command to delete a character from the input string when allowed
        DeleteCharCommand =
            new Command(
                // Command will strip a character from the input string
                () => InputString = InputString.Substring(0, InputString.Length - 1),

                // CanExecute is processed here to return true when there's something to delete
                () => InputString.Length > 0
            );
    }

    string FormatText(string str)
    {
        bool hasNonNumbers = str.IndexOfAny(_specialChars) != -1;
        string formatted = str;

        // Format the string based on the type of data and the length
        if (hasNonNumbers || str.Length < 4 || str.Length > 10)
        {
            // Special characters exist, or the string is too small or large for special formatting
            // Do nothing
        }

        else if (str.Length < 8)
            formatted = string.Format("{0}-{1}", str.Substring(0, 3), str.Substring(3));

        else
            formatted = string.Format("({0}) {1}-{2}", str.Substring(0, 3), str.Substring(3, 3), str.Substring(6));

        return formatted;
    }


    public void OnPropertyChanged([CallerMemberName] string name = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

이 예제에서는 Execute 명령에 대한 메서드와 CanExecute 메서드가 생성자에서 람다 함수로 정의됩니다. viewmodel은 속성이 AddCharCommand 여러 단추(또는 명령 인터페이스가 있는 다른 모든 컨트롤)의 속성에 바인딩 Command 되어 있다고 가정하며, 각 컨트롤은 해당 단추로 CommandParameter식별됩니다. 이러한 단추는 속성에 InputString 문자를 추가한 다음 속성의 전화 번호로 형식이 DisplayText 지정됩니다. 라는 DeleteCharCommand형식 ICommand 의 두 번째 속성도 있습니다. 이 단추는 백 간격 단추에 바인딩되지만 삭제할 문자가 없으면 단추를 사용하지 않도록 설정해야 합니다.

다음 예제에서는 다음을 사용하는 XAML을 KeypadViewModel보여 줍니다.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.KeypadPage"
             Title="Keypad Page">
    <ContentPage.BindingContext>
        <local:KeypadViewModel />
    </ContentPage.BindingContext>

    <Grid HorizontalOptions="Center" VerticalOptions="Center">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="80" />
        </Grid.ColumnDefinitions>

        <Label Text="{Binding DisplayText}"
               Margin="0,0,10,0" FontSize="20" LineBreakMode="HeadTruncation"
               VerticalTextAlignment="Center" HorizontalTextAlignment="End"
               Grid.ColumnSpan="2" />

        <Button Text="&#x21E6;" Command="{Binding DeleteCharCommand}" Grid.Column="2"/>

        <Button Text="1" Command="{Binding AddCharCommand}" CommandParameter="1" Grid.Row="1" />
        <Button Text="2" Command="{Binding AddCharCommand}" CommandParameter="2" Grid.Row="1" Grid.Column="1" />
        <Button Text="3" Command="{Binding AddCharCommand}" CommandParameter="3" Grid.Row="1" Grid.Column="2" />

        <Button Text="4" Command="{Binding AddCharCommand}" CommandParameter="4" Grid.Row="2" />
        <Button Text="5" Command="{Binding AddCharCommand}" CommandParameter="5" Grid.Row="2" Grid.Column="1" />
        <Button Text="6" Command="{Binding AddCharCommand}" CommandParameter="6" Grid.Row="2" Grid.Column="2" />

        <Button Text="7" Command="{Binding AddCharCommand}" CommandParameter="7" Grid.Row="3" />
        <Button Text="8" Command="{Binding AddCharCommand}" CommandParameter="8" Grid.Row="3" Grid.Column="1" />
        <Button Text="9" Command="{Binding AddCharCommand}" CommandParameter="9" Grid.Row="3" Grid.Column="2" />

        <Button Text="*" Command="{Binding AddCharCommand}" CommandParameter="*" Grid.Row="4" />
        <Button Text="0" Command="{Binding AddCharCommand}" CommandParameter="0" Grid.Row="4" Grid.Column="1" />
        <Button Text="#" Command="{Binding AddCharCommand}" CommandParameter="#" Grid.Row="4" Grid.Column="2" />
    </Grid>
</ContentPage>

이 예제에서는 에 Command 바인딩DeleteCharCommand된 첫 번째 Button 속성입니다. 다른 단추는 다음 항목에 AddCharCommand 표시되는 문자와 CommandParameter 동일한 단추를 사용하여 바인딩됩니다 Button.

Screenshot of a calculator using MVVM and commands.