本文介紹在WPF和WinForms中使用ReactiveUI來共用ViewModel。ViewModel來自ReactiveUI說明文件中的LoginViewModel,但加上了ReactiveUI.Fody的使用。
一般來說,你找得到的大多數.Net MVVM Framework都是基於XAML環境的,以前剛看到ReactiveUI時就想測試它的共用性,如下所示。
下列程式碼基本上展示了在WinForm和WPF程式中,共用同一個ViewModel的方式,View主要是呈現一個可選用Guest或是User登入系統的畫面。
ViewModel
//[InjectValidation]
public class LoginViewModel : ReactiveObject
{
[Reactive]
public string User { get; set; }
[Reactive]
public string Password { get; set; }
[Reactive]
public bool IsUserLogin { get; set; }
public ReactiveCommand<Unit, Unit> LoginCommand { get; private set; }
public ReactiveCommand<Unit, Unit> ResetCommand { get; private set; }
//private readonly LoginViewModelValidator _viewModelValidator;
public LoginViewModel()
{
//_viewModelValidator = new LoginViewModelValidator();
// assume a user is going to login
IsUserLogin = true;
// 可登入條件,當使用者及密碼皆有輸入,且密碼長度大於3,或是Guest時可登入系統
var canLogin = this.WhenAny(
vm => vm.User,
vm => vm.Password,
vm => vm.IsUserLogin,
(user, pass, isUser) =>
!string.IsNullOrWhiteSpace(user.Value) &&
!string.IsNullOrWhiteSpace(pass.Value) &&
(pass.Value.Length > 3) ||
!isUser.Value);
// 設定登入命令來源及其條件
LoginCommand = ReactiveCommand.CreateFromObservable(this.LoginAsync, canLogin);
// 當使用者或密碼欄位不為空時,可清除
var canReset = this.WhenAny(
vm => vm.User,
vm => vm.Password,
(user, pass) =>
(!string.IsNullOrWhiteSpace(user.Value) || !string.IsNullOrWhiteSpace(pass.Value))
);
// 設定清除命令動作及其條件
ResetCommand = ReactiveCommand.Create(() =>
{
User = string.Empty;
Password = string.Empty;
}, canReset);
}
// 模擬登入動作
private IObservable<Unit> LoginAsync() =>
Observable
.Return(new Random().Next(0, 2) == 1)
.Delay(TimeSpan.FromSeconds(1))
.Do(
success =>
{
if (!success)
{
Debug.WriteLine("Failed to login");
//MessageBox.Show("Failed to login");
//throw new InvalidOperationException("Failed to login.");
}
}
)
.Select(_ => Unit.Default);
}
Views
XAML
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:SharedViewModel;assembly=SharedViewModel"
xmlns:local="clr-namespace:WpfApp1"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
WindowStartupLocation="CenterScreen"
Title="{x:Static local:MainWindow.WindowTitle}" Height="250" Width="450">
<Window.DataContext>
<vm:LoginViewModel/>
</Window.DataContext>
<Window.Resources>
<local:UserTypeConverter x:Key="UserTypeConverter"/>
<Style TargetType="{x:Type Control}" x:Key="baseStyle">
<Setter Property="FontSize" Value="20" />
</Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource baseStyle}"></Style>
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource baseStyle}"></Style>
<Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource baseStyle}"></Style>
<Style TargetType="{x:Type ListView}" BasedOn="{StaticResource baseStyle}"></Style>
<Style TargetType="{x:Type RadioButton}" BasedOn="{StaticResource baseStyle}"></Style>
</Window.Resources>
<Border Padding="10">
<StackPanel>
<StackPanel.Resources>
<DataTemplate DataType="{x:Type ValidationError}">
<TextBlock FontStyle="Italic" Foreground="Red" HorizontalAlignment="Right" Margin="4" Text="{Binding Path=ErrorContent}" />
</DataTemplate>
</StackPanel.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<RadioButton Grid.Column="0" HorizontalAlignment="Right" GroupName="UserType"
Content="Guest"
Margin="0 0 20 0"
IsChecked="{Binding IsUserLogin,
Converter={StaticResource UserTypeConverter}}"/>
<RadioButton Grid.Column="1" HorizontalAlignment="Left" GroupName="UserType"
Content="Member"
Margin="10 0 0 0"
IsChecked="{Binding IsUserLogin}"/>
</Grid>
<Grid Margin="0 10 0 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Account:" FontSize="18" VerticalAlignment="Center" Grid.Column="0" Margin="0 0 10 0" HorizontalAlignment="Center"/>
<TextBox IsEnabled="{Binding IsUserLogin}" Text="{Binding User, UpdateSourceTrigger=LostFocus, ValidatesOnNotifyDataErrors=True, NotifyOnValidationError=True}" Grid.Column="1" Grid.ColumnSpan="2" Margin="0 0 20 0">
<Validation.ErrorTemplate>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<AdornedElementPlaceholder x:Name="textBox"/>
<TextBox Margin="5" Text="{Binding [0].ErrorContent}" Foreground="Red"></TextBox>
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
</Grid>
<Grid Margin="0 10 0 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Password:" FontSize="18" VerticalAlignment="Center" Grid.Column="0" Margin="0 0 10 0" HorizontalAlignment="Center"/>
<TextBox IsEnabled="{Binding IsUserLogin}" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True, NotifyOnValidationError=True}" Grid.Column="1" Grid.ColumnSpan="2" Margin="0 0 20 0">
<Validation.ErrorTemplate>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<AdornedElementPlaceholder x:Name="textBox"/>
<TextBox Margin="5" Text="{Binding [0].ErrorContent}" Foreground="Red"></TextBox>
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
</Grid>
<Grid Margin="0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Command="{Binding LoginCommand}" Margin="10 0" Grid.Column="1" Content="登入"></Button>
<Button x:Name="btnExit" Click="BtnExit_OnClick" Margin="10 0" Grid.Column="2" Content="離開"></Button>
<Button Command="{Binding ResetCommand}" Grid.Column="3" Content="Reset"></Button>
</Grid>
</StackPanel>
</Border>
</Window>
WinForm
基本上WinForm看起來就跟WPF一樣,不是重點,就不列出了。
Code behind
WPF
/// <summary>
/// MainWindow.xaml 的互動邏輯
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new LoginViewModel();
}
public static string WindowTitle = "Login";
public static string ShowText { get { return "show text"; } }
private void BtnExit_OnClick(object sender, RoutedEventArgs e)
{
this.Close();
}
}
WinForm
public partial class LoginForm : Form, IViewFor<LoginViewModel>
{
public LoginForm()
{
InitializeComponent();
ViewModel = new LoginViewModel();
// 將ViewModel的屬性綁定至UI控制項上
this.Bind(ViewModel, x => x.IsUserLogin, x => x.rdoUser.Checked);
this.Bind(ViewModel, x => x.User, x => x.txtAccount.Text);
this.Bind(ViewModel, x => x.Password, x => x.txtPassword.Text);
// 將ViewModel的命令綁定至UI控制項
this.BindCommand(ViewModel, x => x.LoginCommand, x => x.btnLogin);
this.BindCommand(ViewModel, x => x.ResetCommand, x => x.btnReset);
this.OneWayBind(ViewModel, x => x.IsUserLogin, x => x.txtAccount.Enabled);
this.OneWayBind(ViewModel, x => x.IsUserLogin, x => x.txtPassword.Enabled);
/*
this.OneWayBind(ViewModel,
x => x.IsUserLogin,
x => x.txtAccount.BackColor,
x => x ? Color.Green : Color.BlueViolet); */
}
object IViewFor.ViewModel
{
get { return ViewModel; }
set { ViewModel = (LoginViewModel)value; }
}
public LoginViewModel ViewModel { get; set; }
private void btnExit_Click(object sender, EventArgs e)
{
this.Close();
}
}
基本上,ReactiveUI在這兩種GUI架構上的差別僅在WinForm要有一個綁定的動作,而資料驅動導向的WPF基本上在XAML中就直接完成綁定的動作。不過在WinForm上到是帶來了很多的好處,在資料綁定的部份不再產生”MagicString”(以前在處理類似功能時,還特別找其它的函式庫來應用);更可以直接綁定控制項和命令,減少了很多View端的CodeBehind程式碼。
而在面對多個控制項的不同狀態的互動時,一般可能會採用類Mediator模式來處理,現在也可以透過自訂狀態類別來對映至各個不同控制項,讓程式更簡潔。
Written with StackEdit.