Xamarin.Forms 데이터 바인딩 3/3

이전 시리즈에서 기본적인 데이터 바인딩을 이용하여 Xamarin.Forms 어플리케이션을 개발할때 최소한의 노력으로 앱의 데이터와 UI가 동기화 할 수 있다는것을 확인하였다. 다음에는, 좀 더 진보된 바인딩 시나리오에 따라 소스와 타깃객체 사이에 값을 전달시 변환하는 방법도 보았다.
이번 글에서는, Xamarin.Forms의 커맨딩이라는 기능을 소개할것이다. 이것은 뷰모델에 직접 메소드 호출을 하여 데이터 바인딩이 이루어지도록 해준다.

커맨딩(Commanding) 소개

기존의 메소드 호출 방법은 일반적으로 버튼객체에 Clicked 이벤트 핸들러나 TapGestureRecognizer의 Tapped 이벤트 핸들러로부터 메소드를 실행하는 형태였다. 하지만, 커맨딩을 이용하면 다음의 UI객체들로 부터 뷰모델에 직접 메소드 호출을 하여 데이터를 바인딩할 수 있다.

  • Button
  • MenuItem
  • ToolbarItem
  • TextCell
  • ImageCell
  • ListView
  • TapGestureRecognizer

커맨딩을 지원하기 위해서, 주요 클래스들에 두개의 퍼블릭 속성이 정의되어있다.

  • ICommand (System.Window.Input.ICommand)
  • CommandParameter (object)

커맨드 구현

커맨딩을 구현하기 위해서, 뷰모델은 하나 이상의 ICommand를 정의하여야 한다. ICommand 인터페이스는 두개의 메소드와 하나의 이벤트로 구성되어 있다.

1
2
3
4
5
6
public interface ICommand
{
void Execute(object arg);
bool CanExecute(object arg);
event EventHandler CanExecuteChanged;
}

Xamarin.Forms에서 제공되는 Command와 Command<T>는 ICommand 인터페이스의 구현체이다. “T”는 Execute와 CanExecute의 인자 타입이다. 추가로, 이 클래스들은 ChangeCanExecute 메소드를 포함하고 있다. 이 메소드는 커멘드 객체로 부터 CanExecuteChanged 이벤트를 발생시킨다.
뷰모델 내부의, ICommand타입의 커맨드 객체는 Command 또는 Command 타입의 생성자와 Action 콜백 객체를 필요로 한다. 이 Action은 Button이 ICommand.Execute 메소드를 호출할때 불리워진다. CanExecute 메소드는 선택적인 생성자 인자로서, bool값을 리턴하는 함수 형태를 취한다.
다음은, 제곱근을 계산하는 예제의 뷰모델 코드 일부이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DemoViewModel : INotifyPropertyChanged
{
public int Number { get; set; }
public double SquareRootResult { get; private set; }
public ICommand SquareRootCommand { get; private set; }
...
public DemoViewModel ()
{
Number = 25;
SquareRootCommand = new Command (CalculateSquareRoot);
...
}
void CalculateSquareRoot ()
{
SquareRootResult = Math.Sqrt (Number);
OnPropertyChanged ("SquareRootResult");
}
...
}

SquareRootCommand는 Button의 Command속성으로서 CalculateSqureRoot라는 Action을 콜백으로 가진 바인딩 속성이다. 예제 XAML을 통해 이 뷰모델이 이떻게 사용되는지 보자.

1
2
3
4
5
6
7
8
9
10
<Label Text="Demo 1 - Command" FontAttributes="Bold" />
<StackLayout Orientation="Horizontal">
<Label Text="Enter number:" />
<Entry Text="{Binding Number, Mode=TwoWay}" WidthRequest="50" />
</StackLayout>
<Button Text="Calculate Square Root" Command="{Binding SquareRootCommand}" HorizontalOptions="Center" />
<StackLayout Orientation="Horizontal">
<Label Text="Square root =" />
<Label Text="{Binding SquareRootResult}" />
</StackLayout>

입력값은 Number와 양방향으로 바인딩되고, 버튼을 클릭하면 입력값에 따라 SquareRootCommand가 실행되어 SquareRootResult가 Label에 Text속성으로 적용된다.
실행결과

커맨드에 파라미터 전달하기

Command 클래스에 인자의 타입을 지정해 ICommand.Execute에 인자를 전달할 수 있다. 아래 코드는 제곱근을 계산할 입력값을 커맨드 인자로 전달하는 이제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DemoViewModel : INotifyPropertyChanged
{
public double SquareRootWithParameterResult { get; private set; }
public ICommand SquareRootWithParameterCommand { get; private set; }
...
public DemoViewModel ()
{
SquareRootWithParameterCommand = new Command<string> (CalculateSquareRoot);
...
}
void CalculateSquareRoot (string value)
{
double num = Convert.ToDouble (value);
SquareRootWithParameterResult = Math.Sqrt (num);
OnPropertyChanged ("SquareRootWithParameterResult");
}
...
}

Button의 ICommand 속성에 SquareRootWithParameterCommand가 바운드 되어있다. 이 속성은 Command 타입으로 CalcualteSqureRoot 메소드에 string value 를 인자로 전달할 수 있다. 다음 코드는 이것을 사용한 XAML 소스 이다.

1
2
3
4
5
6
7
8
9
10
11
12
<Label Text="Demo 2 - Command with Parameter" FontAttributes="Bold" />
<StackLayout Orientation="Horizontal">
<Label Text="Enter number:" />
<Entry x:Name="entry" Text="100" WidthRequest="50" />
</StackLayout>
<Button Text="Calculate Square Root"
Command="{Binding SquareRootWithParameterCommand}"
CommandParameter="{Binding Source={x:Reference entry}, Path=Text}" HorizontalOptions="Center" />
<StackLayout Orientation="Horizontal">
<Label Text="Square root =" />
<Label Text="{Binding SquareRootWithParameterResult}" />
</StackLayout>

이전의 소스와 차이점은 Button의 CommandParameter이다. 인자로, entry의 Text속성이 인자의 값으로 바인딩 되었음을 볼 수 있다.

비동기 메소드 호출

커맨드는 Action콜백에 async와 await 키워드를 사용해 비동기 메소드 호출도 지원한다. 콜백은 Task를 리턴하므로 반드시 기다려야 한다는것을 의미한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class DemoViewModel : INotifyPropertyChanged
{
...
bool canDownload = true;
string simulatedDownloadResult;
public string SimulatedDownloadResult {
get { return simulatedDownloadResult; }
private set {
if (simulatedDownloadResult != value) {
simulatedDownloadResult = value;
OnPropertyChanged ("SimulatedDownloadResult");
}
}
}
public ICommand SimulateDownloadCommand { get; private set; }
public DemoViewModel ()
{
...
SimulateDownloadCommand =
new Command (async () => await SimulateDownloadAsync (), () => canDownload);
}
async Task SimulateDownloadAsync ()
{
CanInitiateNewDownload (false);
SimulatedDownloadResult = string.Empty;
await Task.Run (() => SimulateDownload ());
SimulatedDownloadResult = "Simulated download complete";
CanInitiateNewDownload (true);
}
void CanInitiateNewDownload (bool value)
{
canDownload = value;
((Command)SimulateDownloadCommand).ChangeCanExecute ();
}
void SimulateDownload ()
{
// Simulate a 5 second pause
var endTime = DateTime.Now.AddSeconds (5);
while (true) {
if (DateTime.Now >= endTime) {
break;
}
}
}
...
}

SimulateDownloadCommand는 버튼의 Command속성에 바운드 되었다. XAML코드는 아래와 같이 선언되어 있다.

1
2
3
4
5
6
7
<Label Text="Demo 3 - Async Command with CanExecute" FontAttributes="Bold" />
<Button Text="Simulate 5 Second Download" HorizontalOptions="Center"
Command="{Binding SimulateDownloadCommand}" />
<StackLayout Orientation="Horizontal">
<Label Text="Result: " />
<Label Text="{Binding SimulatedDownloadResult}" />
</StackLayout>

버튼이 클릭되면 SimualteDownloadAsync 메소드가 실행되고 5초동안 버튼은 비활성화 되었다가 다시 활성화가 된다. 이것은 ICommand.CanExecute 메소드가 SimulateDownloadCommand 생성자의 두번째 인자로 설정되었기 때문이다. 버튼은 CanExecute를 먼저 체크한다. 만일, false라면 버튼은 비활성화되고 Execute는 호출되지 않는다. 또한, 버튼은 ICommand.CanExecuteChanged 이벤트를 처리한다. 만일, 뷰모델에서 Command.CanExecuteChange 메소드가 호출되어 CanExecuteChanged 이벤트가 발생한다면 버튼은 CanExecute 메소드를 다시 체크하여 버튼의 활성화 및 Execute 메소드의 실행가능 여부를 재평가하게 된다.
비동기 커맨드 예제
이 예제에서 다운로딩이 별도의 쓰레드에서 진행되는 도중에도 UI객체와 교류가 가능함에 주목하도록 하자.

정리

커맨딩 기능을 이용한 데이터 바인딩은 뷰 모델 구현이 좀 복잡해 보일 수 있으나,컴포넌트의 재활용 측면에서 MVVM에 매우 적합한 코딩패턴이다. 또한, 실제 코딩시 비동기 호출이 많이 사용되는데 이 부분 또한 깔끔하게 처리할 수 있을 뿐만 아니라 데이터 모델과 바인딩 처리가 한곳에서 이루어 짐으로써 코드의 유지 관리 측면에서도 유리하다.

원본출처 : https://blog.xamarin.com/simplifying-events-with-commanding/