2022. 8. 1. 00:00ㆍMAUI
일반적으로 간단한 속성 변경에 대한 event handler 들이 있다.
OnChangedXXXX, OnCreatedXXXX, OnDeletedXXXX 등등 View 의 특성에 따라 존재한다.
하지만 사용자의 어떤한 경우는 요구사항이 복잡한 때도 있다.
이럴 경우에 command 인터페이스를 이용한다.
Button과 viewmodel 간의 데이터 바인딩을 허용하기 위해 Button은 두 가지 속성을 정의한다.
Command
: View 에 특정 Command 가 발생했다고 전달
CommandParameter
: Command 에 필요한 parameter 전달
이렇게 Command 로 View 와 binding 하게 되면 Behind 코드에 OnChangedXXXX 등과 강하게 결합되지 않고 ViewModel 과 연결되어 좀더 느슨한 결합 상태가 된다. 이를 통해 의존성을 줄일 수 있다.
ICommand 인터페이스는 두 가지 메서드와 하나의 이벤트로 구성된다.
public interface ICommand
{
public void Execute (Object parameter);
public bool CanExecute (Object parameter);
public event EventHandler CanExecuteChanged;
}
viewmodel 에서 다음과 같이 사용한다.
public ICommand MyCommand { private set; get; }
XAML 에서는 아래와 같이 연결한다.
<Button Text="Execute command"
Command="{Binding MyCommand}" />
사용자가 Button을 누르면 Button은 Command 속성에 바인딩된 ICommand 개체의 Execute 메서드를 호출한다.
데이터 바인딩이 어떤 식으로든 변경되면 ICommand 개체에서 CanExecute 메서드를 호출한다.
CanExecute가 false를 반환하면 Button 자체가 비활성화된다.
이것은 특정 명령이 현재 사용할 수 없거나 유효하지 않음을 나타낸다.
또한 Button은 ICommand의 CanExecuteChanged 이벤트에 대한 처리기를 연결한다.
이벤트는 viewmodel 내에서 발생하고 해당 이벤트가 발생하면 Button이 CanExecute를 다시 호출한다.
CanExecute가 true를 반환하면 자체적으로 활성화되고 false를 반환하면 자체적으로 비활성화된다.
예제
public class PersonViewModel : INotifyPropertyChanged
{
string name;
double age;
string skills;
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
set { SetProperty(ref name, value); }
get { return name; }
}
public double Age
{
set { SetProperty(ref age, value); }
get { return age; }
}
public string Skills
{
set { SetProperty(ref skills, value); }
get { return skills; }
}
public override string ToString()
{
return Name + ", age " + Age;
}
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Collection 같은 경우는 아래와 같이 표현 가능 하다
IList<T> 형태를 ObservablCollection 으로 만들어야 한다.
이렇게 해야 collection 의 변화를 탐지하여 xaml 에 반영한다.
public class PersonCollectionViewModel : INotifyPropertyChanged
{
PersonViewModel personEdit;
bool isEditing;
public event PropertyChangedEventHandler PropertyChanged;
···
public bool IsEditing
{
private set { SetProperty(ref isEditing, value); }
get { return isEditing; }
}
public PersonViewModel PersonEdit
{
set { SetProperty(ref personEdit, value); }
get { return personEdit; }
}
public ICommand NewCommand { private set; get; }
public ICommand SubmitCommand { private set; get; }
public ICommand CancelCommand { private set; get; }
public IList<PersonViewModel> Persons { get; } = new ObservableCollection<PersonViewModel>();
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiApp1.Commanding.MainPage"
xmlns:local="clr-namespace:MauiApp1.Commanding"
Title="MainPage">
<ContentPage.BindingContext>
<local:PersonCollectionViewModel />
</ContentPage.BindingContext>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- New Button -->
<Button Text="New"
Grid.Row="0"
Command="{Binding NewCommand}"
HorizontalOptions="Start" />
<!-- Entry Form -->
<Grid Grid.Row="1"
IsEnabled="{Binding IsEditing}">
<Grid BindingContext="{Binding PersonEdit}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Text="Name: " Grid.Row="0" Grid.Column="0" />
<Entry Text="{Binding Name}"
Grid.Row="0" Grid.Column="1" />
<Label Text="Age: " Grid.Row="1" Grid.Column="0" />
<StackLayout Orientation="Horizontal"
Grid.Row="1" Grid.Column="1">
<Stepper Value="{Binding Age}"
Maximum="100" />
<Label Text="{Binding Age, StringFormat='{0} years old'}"
VerticalOptions="Center" />
</StackLayout>
<Label Text="Skills: " Grid.Row="2" Grid.Column="0" />
<Entry Text="{Binding Skills}"
Grid.Row="2" Grid.Column="1" />
</Grid>
</Grid>
<!-- Submit and Cancel Buttons -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Text="Submit"
Grid.Column="0"
Command="{Binding SubmitCommand}"
VerticalOptions="Center" />
<Button Text="Cancel"
Grid.Column="1"
Command="{Binding CancelCommand}"
VerticalOptions="Center" />
</Grid>
<!-- List of Persons -->
<ListView Grid.Row="3"
ItemsSource="{Binding Persons}" />
</Grid>
</ContentPage>
사용자가 처음으로 새로 만들기 버튼을 누르면 입력 양식이 활성화되지만 새로 만들기 버튼은 비활성화된다.
그런 다음 사용자는 이름, 나이 및 기술을 입력한다.
편집 중 언제든지 사용자는 취소 버튼을 눌러 다시 시작할 수 있다.
이름과 유효한 나이를 입력한 경우에만 제출 버튼이 활성화된다.
이 제출 버튼을 누르면 사람이 ListView에 표시되는 컬렉션으로 전송된다.
취소 또는 제출 버튼을 누르면 입력 양식이 지워지고 새로 만들기 버튼이 다시 활성화된다.
각각의 command 가 viewmodel 을 통해 위의 작업들을 처리한다.
...
NewCommand = new Command(
execute: () =>
{
PersonEdit = new PersonViewModel();
PersonEdit.PropertyChanged += OnPersonEditPropertyChanged;
IsEditing = true;
RefreshCanExecutes();
},
canExecute: () =>
{
return !IsEditing;
});
SubmitCommand = new Command(
execute: () =>
{
Persons.Add(PersonEdit);
PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
PersonEdit = null;
IsEditing = false;
RefreshCanExecutes();
},
canExecute: () =>
{
return PersonEdit != null &&
PersonEdit.Name != null &&
PersonEdit.Name.Length > 1 &&
PersonEdit.Age > 0;
});
CancelCommand = new Command(
execute: () =>
{
PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
PersonEdit = null;
IsEditing = false;
RefreshCanExecutes();
},
canExecute: () =>
{
return IsEditing;
});
...
Command Parameters 사용하기
Command 를 공유하는 경우 CommandParameter 속성을 사용하여 View 를 구분할 수 있다.
CommandParameter 은 다음과 같이 사용한다.
<Button Text="0"
Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"
Command="{Binding DigitCommand}"
CommandParameter="0" />
<Button Text="·"
Grid.Row="5" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="." />
decimal number 를 입력하는 keyboard 를 생각해보자
XAML
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiApp1.Commanding.DecimalKeyPad"
xmlns:local="clr-namespace:MauiApp1.Commanding"
Title="DecimalKeyPad">
<ContentPage.BindingContext>
<local:DecimalKeypadViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<Style TargetType="Button">
<Setter Property="FontSize" Value="32" />
<Setter Property="BorderWidth" Value="1" />
<Setter Property="BorderColor" Value="Black" />
</Style>
</ContentPage.Resources>
<Grid WidthRequest="240"
HeightRequest="480"
ColumnDefinitions="80, 80, 80"
RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto"
ColumnSpacing="2"
RowSpacing="2"
HorizontalOptions="Center"
VerticalOptions="Center">
<Label Text="{Binding Entry}"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"
Margin="0,0,10,0"
FontSize="32"
LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center"
HorizontalTextAlignment="End" />
<Button Text="CLEAR"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
Command="{Binding ClearCommand}" />
<Button Text="⇦"
Grid.Row="1" Grid.Column="2"
Command="{Binding BackspaceCommand}" />
<Button Text="7"
Grid.Row="2" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="7" />
<Button Text="8"
Grid.Row="2" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="8" />
<Button Text="9"
Grid.Row="2" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="9" />
<Button Text="4"
Grid.Row="3" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="4" />
<Button Text="5"
Grid.Row="3" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="5" />
<Button Text="6"
Grid.Row="3" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="6" />
<Button Text="1"
Grid.Row="4" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="1" />
<Button Text="2"
Grid.Row="4" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="2" />
<Button Text="3"
Grid.Row="4" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="3" />
<Button Text="0"
Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"
Command="{Binding DigitCommand}"
CommandParameter="0" />
<Button Text="."
Grid.Row="5" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="." />
</Grid>
</ContentPage>
10자리의 11개 버튼과 소수점은 DigitCommand에 대한 바인딩을 공유한다.
CommandParameter 은 이러한 버튼을 구분하는 역할을 한다.
ViewModel 은 ClearCommand 와 BackspaceCommand 를 제외 하고 DigitCommand 를 공유한다.
using System.ComponentModel;
using System.Windows.Input;
namespace MauiApp1.Commanding;
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
string entry = "0";
public event PropertyChangedEventHandler PropertyChanged;
public DecimalKeypadViewModel()
{
ClearCommand = new Command(
execute: () =>
{
Entry = "0";
RefreshCanExecutes();
});
BackspaceCommand = new Command(
execute: () =>
{
Entry = Entry.Substring(0, Entry.Length - 1);
if (Entry == "")
{
Entry = "0";
}
RefreshCanExecutes();
},
canExecute: () =>
{
return Entry.Length > 1 || Entry != "0";
});
DigitCommand = new Command<string>(
execute: (string arg) =>
{
Entry += arg;
if (Entry.StartsWith("0") && !Entry.StartsWith("0."))
{
Entry = Entry.Substring(1);
}
RefreshCanExecutes();
},
canExecute: (string arg) =>
{
return !(arg == "." && Entry.Contains("."));
});
}
void RefreshCanExecutes()
{
((Command)BackspaceCommand).ChangeCanExecute();
((Command)DigitCommand).ChangeCanExecute();
}
public string Entry
{
private set
{
if (entry != value)
{
entry = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Entry"));
}
}
get
{
return entry;
}
}
public ICommand ClearCommand { private set; get; }
public ICommand BackspaceCommand { private set; get; }
public ICommand DigitCommand { private set; get; }
}
BackspaceCommand 와 DigitCommand 는 canExecute 를 구현하여 Button 에 enable 을 구현한다.
ClearCommand 는 항상 enable 상태이다.
DigitCommand 는 Command<string> 를 통해 CommandParameter 를 입력 받는다.
arg 에 CommandParameter 에 담은 string 문자열이 전달된다.
canExecute 의 내용은 Entry 를 check 하여 이미 소수점이 포함되어 있다면
해당 button 을 disable 하게 만든다.
그렇지 않다면 해당 button 은 true 일 것이다.
관련영상
'MAUI' 카테고리의 다른 글
.NET MAUI - Data Templates (0) | 2022.08.03 |
---|---|
.NET MAUI - Control Templates (0) | 2022.08.02 |
.NET MAUI - Binding Relative bindings (0) | 2022.07.29 |
.NET MAUI - Binding Value Converters (0) | 2022.07.28 |
.NET MAUI - Binding 경로 (Path) (0) | 2022.07.27 |