MVVM에 대한 LoongCore 심층 이해
최고의 심층 지식, 실습
기사 디렉토리
MVVM 프레임워크의 함의를 이해하기 위한 개인 손 자위 및 단위 테스트를 통해
12.추상 및 단위 테스트 초기 경험
- ObservableObject 클래스 정의
이것이 미래 ViewModel의 기본 클래스이므로 실질적인 의미가 없으며 추상 클래스로 정의되어야 하지만 여기서는 의도적으로 잊어버렸습니다.
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
}
-
새 단위 테스트 만들기
[외부 링크 사진 전송 실패, 소스 사이트에 거머리 방지 메커니즘이 있을 수 있습니다. 사진을 저장하고 직접 업로드하는 것이 좋습니다(img-8N3u93EF-1667689822363)(Figures/01.IsAbstract.png) ] -
단위 테스트 프로젝트에서 ObservableObject가 추상 클래스인지 확인합니다.
[TestClass]
public class ObservableObject_Test
{
[TestMethod]
public void IsAbstract() {
var type = typeof(ObservableObject);
Assert.IsTrue(type.IsAbstract);
}
}
-
테스트 탐색기에서 테스트 실행
[외부 링크 사진 전송 실패, 소스 사이트에 거머리 방지 메커니즘이 있을 수 있습니다. 사진을 저장하고 직접 업로드하는 것이 좋습니다(img-RVJw8Wjq-1667689822364)(Figures/01.IsAbstractTest. png)] -
ObservableObject 수정 후 테스트 통과
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
}
13. PropertyChanged 속성이 변경되면 어떻게 됩니까?
ObservableObject.cs
속성이 변경될 때 이벤트를 발생시키는 기본 메서드
// TODO: 13-1 引发属性改变事件的方法
/// <summary>
/// 引发属性改变事件
/// </summary>
/// <param name="propertyName">发生改变的属性的名称</param>
/// <remarks>
/// ?. 操作符如果有人给ViewModel留了“名片”才会引发,即外部有人订阅了PropertyChanged
/// 没有这个方法是可以的,但是你可能得硬编码写propertyName
/// </remarks>
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke
(
this,
new PropertyChangedEventArgs(propertyName)
);
ObservableObject.cs
설정할 값이 실제로 새 값일 때 RaisePropertyChanged를 호출하는 속성 설정자
// TODO: 13-2 属性设置器
/// <summary>
/// 设置新的属性值,如果是“真的新”,调用<seealso cref="RaisePropertyChanged(string)"/>
/// </summary>
/// <typeparam name="T">目标属性的类型</typeparam>
/// <param name="target">目标属性</param>
/// <param name="value">可能是新的值</param>
/// <param name="propertyName">[不要设置]目标属性的名称,自动推断</param>
/// <returns>[true]目标属性已被更新?</returns>
protected bool SetProperty<T>
(
ref T target, // 目标属性
T value, // “新”值
[CallerMemberName] string propertyName = null
) {
if (EqualityComparer<T>.Default.Equals(target, value))
return false;
target = value;
RaisePropertyChanged(propertyName);
return true;
}
- 단위 테스트
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
// TODO: 13-3 引用Logger记录器
using LoongEgg.LoongLogger;
namespace LoongEgg.LoongCore.Test
{
[TestClass]
public class ObservableObject_Test
{
// TODO: 13-4 测试初始化
/// <summary>
/// 初始化测试,会在所有测试方法前调用
/// </summary>
/// <remarks>
/// LoongEgg.LoongLogger是我的一个开源项目,你可以不使用
/// </remarks>
[TestInitialize]
public void EnabledLogger() {
LoggerManager.Enable(LoggerType.File, LoggerLevel.Debug);
}
/// <summary>
/// 抽象类确认
/// </summary>
[TestMethod]
public void IsAbstract() {
var type = typeof(ObservableObject);
Assert.IsTrue(type.IsAbstract);
}
// TODO: 13-5 设计一个测试类
/// <summary>
/// <see cref="ObservableObject"/>的一个测试样本
/// </summary>
public class ObservableObjectSample : ObservableObject
{
/// <summary>
/// 测试属性
/// </summary>
public int PropertySample {
get => _PropertySample;
set => SetProperty(ref _PropertySample, value);
}
/// <summary>
/// 测试字段
/// </summary>
private int _PropertySample;
}
// TODO: 13-6 属性改变时会发生什么
/// <summary>
/// 属性改变,且会引发事件确认
/// </summary>
[TestMethod]
public void CanPropertyChangedRaised() {
bool isPropertyChangeRaised = false;// 事件引发标记
// 初始化一个检测样本
ObservableObjectSample sample = new ObservableObjectSample();
// 注册属性改变时的处理事件
sample.PropertyChanged += (s, args) =>
{
isPropertyChangeRaised = true;
LoggerManager.WriteDebug($"PropertyName:{args.PropertyName}");
};
// 改变属性
sample.PropertySample = 666;
Assert.IsTrue(isPropertyChangeRaised);
Assert.AreEqual(sample.PropertySample, 666);
}
// TODO: 13-7 清理测试环境
/// <summary>
/// 在所有测试完成后调用,注销LoggerManager
/// </summary>
[TestCleanup]
public void DisableLogger() {
LoggerManager.WriteDebug("LoggerManager is clean up...");
LoggerManager.Disable();
}
}
}
14. 적시에 알림 트리거
- 속성이 실제로 변경되지 않는 경우 이벤트를 실행하지 않음
// TODO: 14-1 当“新值”等于当前值时不引发通知
/// <summary>
/// 当“新值”等于当前值时不引发通知
/// </summary>
public void WhenPropertyEqualsOldValue_NotRaised() {
bool isPropertyChangeRaised = false;// 事件引发标记
// 初始化一个检测样本
// 注意这里赋了一个初始值
ObservableObjectSample sample = new ObservableObjectSample { PropertySample = 666};
// 注册属性改变时的处理事件
sample.PropertyChanged += (s, args) =>
{
isPropertyChangeRaised = true;
LoggerManager.WriteDebug(
$"Event is raised by PropertyName={args.PropertyName}, value={sample.PropertySample}");
};
// 改变属性
sample.PropertySample = 666;
Assert.IsFalse(isPropertyChangeRaised); // 注意这里断言是Flase
Assert.AreEqual(sample.PropertySample, 666);
}
- ViewModelBase.cs 만들기
// TODO: 14-2 设计ViewModel的基类
/// <summary>
/// ViewModel们继承于此
/// </summary>
public abstract class ViewModelBase : ObservableObject { }
- 다른 사람의 속성 변경 이벤트에 따라 종속에 의해 트리거됨
[외부 링크 사진 전송 실패, 소스 사이트에 도난 방지 링크 메커니즘이 있을 수 있으므로 사진을 저장하고 직접 업로드하는 것이 좋습니다(img-kiDBfLR2-1667689822364) (Figures/14.People.png)] 테스트
추가 ViewModelBase_Test.cs
수업
public class People: ViewModelBase
{
public string FamilyName {
get => _FamilyName;
set {
if (SetProperty(ref _FamilyName, value))
RaisePropertyChanged("FullName");
}
}
private string _FamilyName = "[NotDefined]";
public string LastName {
get => _LastName;
set {
if (SetProperty(ref _LastName, value))
RaisePropertyChanged(nameof(FullName));
}
}
private string _LastName = "[Unknown]";
public string FullName => $"{FamilyName} - {LastName}";
}
4. 완전한 단위 테스트
using System;
using LoongEgg.LoongLogger;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace LoongEgg.LoongCore.Test
{
[TestClass]
public class ViewModelBase_Test
{ /// <summary>
/// 初始化测试,会在所有测试方法前调用
/// </summary>
/// <remarks>
/// LoongEgg.LoongLogger是我的一个开源项目,你可以不使用
/// </remarks>
[TestInitialize]
public void EnabledLogger() {
LoggerManager.Enable(LoggerType.File, LoggerLevel.Debug);
LoggerManager.WriteDebug("Test initialized ok ....");
}
// TODO: 14-3设计测试类People
public class People: ViewModelBase
{
public string FamilyName {
get => _FamilyName;
set {
if (SetProperty(ref _FamilyName, value))
RaisePropertyChanged("FullName");
}
}
private string _FamilyName = "[NotDefined]";
public string LastName {
get => _LastName;
set {
if (SetProperty(ref _LastName, value))
RaisePropertyChanged(nameof(FullName));
}
}
private string _LastName = "[Unknown]";
public string FullName => $"{FamilyName} - {LastName}";
}
// TODO: 14-4 检查可以强制引发属性改变事件
[TestMethod]
public void CanRaisedByOtherProperty() {
People people = new People();
bool isRaised = false;
people.PropertyChanged += (s, e) =>
{
isRaised = true;
if(e.PropertyName == "FullName") {
LoongLogger.LoggerManager.WriteDebug($"FullName is changed to -> {people.FullName}");
}
};
people.FamilyName = "Alpha";
people.LastName = "Jet";
Assert.IsTrue(isRaised);
}
/// <summary>
/// 在所有测试完成后调用,注销LoggerManager
/// </summary>
[TestCleanup]
public void DisableLogger() {
LoggerManager.WriteDebug("LoggerManager is clean up...");
LoggerManager.Disable();
}
}
}
15. ICommand 명령 구현
- ICommand 구현
DelegateCommand.cs
using System;
using System.Windows.Input;
/*
| 个人微信:InnerGeeker
| 联系邮箱:[email protected]
| 创建时间:2020/4/12 18:28:22
| 主要用途:
| 更改记录:
| 时间 版本 更改
*/
namespace LoongEgg.LoongCore
{
public class DelegateCommand : ICommand
{
/*---------------------------------------- Fields ---------------------------------------*/
/// <summary>
/// 干活的方法
/// </summary>
private readonly Action<object> _Execute;
/// <summary>
/// 判断可以干活的方法
/// </summary>
private readonly Predicate<object> _CanExecute;
public bool CanExecuteCache { get; private set; } = true;
/*------------------------------------- Constructors ------------------------------------*/
/// <summary>
/// 主构造器
/// </summary>
/// <param name="execute">干活的方法</param>
/// <param name="canExecute">判断可以干活的方法</param>
public DelegateCommand(Action<object> execute, Predicate<object> canExecute) {
_Execute = execute ?? throw new ArgumentNullException("execute 不能为空");
_CanExecute = canExecute;
}
/// <summary>
/// 构造器
/// </summary>
/// <param name="execute">干活的方法</param>
public DelegateCommand(Action<object> execute) : this(execute, null) { }
public event EventHandler CanExecuteChanged;
/*------------------------------------ Public Methods -----------------------------------*/
/// <summary>
/// 检查是否可以执行命令
/// </summary>
/// <param name="parameter"></param>
/// <returns></returns>
public bool CanExecute(object parameter) {
bool canExecute = _CanExecute?.Invoke(parameter) ?? true;
if(canExecute != CanExecuteCache) {
CanExecuteCache = canExecute;
RaiseCanExecuteChanged();
}
return canExecute;
}
/// <summary>
/// 执行命令操作
/// </summary>
/// <param name="parameter"></param>
public void Execute(object parameter) => _Execute(parameter);
/// <summary>
/// 引发可执行改变事件
/// </summary>
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
2. DelegateCommand 단위 테스트
using System;
using LoongEgg.LoongLogger;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace LoongEgg.LoongCore.Test
{
[TestClass]
public class DelegateCommand_Test
{
/// <summary>
/// 初始化测试,会在所有测试方法前调用
/// </summary>
/// <remarks>
/// LoongEgg.LoongLogger是我的一个开源项目,你可以不使用
/// </remarks>
[TestInitialize]
public void EnabledLogger() {
LoggerManager.Enable(LoggerType.File, LoggerLevel.Debug);
LoggerManager.WriteDebug("Test initialized ok ....");
}
/// <summary>
/// 检查在构造器中execute不能为null
/// </summary>
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Constructor_ThrowExectptionIfActionParameterIsNull() {
var command = new DelegateCommand(null);
}
/// <summary>
/// Action可以被正常委托执行
/// </summary>
[TestMethod]
public void ExecuteAction_CanInvokes() {
bool invoked = false;
void action(object obj) => invoked = true;
var command = new DelegateCommand(action);
command.Execute(null);
Assert.IsTrue(invoked);
}
/// <summary>
/// CanExecute为Null时命令默认可以执行
/// </summary>
[TestMethod]
public void CanExecute_IsTrueByDefault() {
var command = new DelegateCommand(obj => { });
Assert.IsTrue(command.CanExecute(null));
}
/// <summary>
/// CanExecute可以判断命令不能执行
/// </summary>
[TestMethod]
public void CanExecute_FalsePredicate() {
var command = new DelegateCommand
(
obj => { },
obj => (int)obj == 0
);
Assert.IsFalse(command.CanExecute(6));
}
/// <summary>
/// CanExecute可以判断命令可以执行
/// </summary>
[TestMethod]
public void CanExecute_TruePredicate() {
var command = new DelegateCommand
(
obj => { },
obj => (int)obj == 6
);
Assert.IsTrue(command.CanExecute(6));
}
[TestMethod]
public void CanExecuteChanged_Raised() {
var command = new DelegateCommand
(
obj => { },
obj => (int)obj == 6
);
bool isCanExecuteChanged = false;
command.CanExecuteChanged += (s, e) =>
{
isCanExecuteChanged = true;
LoggerManager.WriteDebug($"CanExecuteChanged Raised by {s.ToString()}");
};
Assert.IsTrue(command.CanExecute(6));
Assert.IsFalse(command.CanExecute(66));
Assert.IsTrue(isCanExecuteChanged);
}
/// <summary>
/// 在所有测试完成后调用,注销LoggerManager
/// </summary>
[TestCleanup]
public void DisableLogger() {
LoggerManager.WriteDebug("LoggerManager is clean up...");
LoggerManager.Disable();
}
}
}
16. 내 MVVM 프로젝트 구조 및 콘솔에서 WPF 시작
1. 내 프로젝트 구조
AppConsole
View와 ViewModel을 조립하는 콘솔 프로그램LoongEgg.LoongCore
공통 클래스 라이브러리인 MVVM 코어 프레임워크는 ViewModel의 기본 클래스를 제공합니다.LoongEgg.LoongCore.Test
핵심 프레임워크에 대한 단위 테스트 프로젝트LoongEgg.ViewModels
일반 클래스 라이브러리, ViewModel이 여기에 설계되었으며 소규모 프로젝트도 비즈니스 로직 처리를 담당합니다.LoongEgg.ViewModels.Test
ViewModel의 단위 테스트LoongEgg.Views
Custom control library, Views are design in this episode
[외부 링크 이미지 전송 실패, 소스 사이트에 거머리 방지 메커니즘이 있을 수 있습니다. 이미지를 저장하고 직접 업로드하는 것이 좋습니다(img-2q0z4VMa-1667689822364)(그림/16) .ProjectLayout.png) ]
2. 콘솔에서 WPF 창을 시작합니다.
- 필요한 참조
[외부 링크 사진 전송 실패, 소스 사이트에 거머리 방지 메커니즘이 있을 수 있습니다. 사진을 저장하고 직접 업로드하는 것이 좋습니다(img-s4lE1leM-1667689822365)(Figures/16.Reference.png)] - Program.cs
using LoongEgg.ViewModels;
using LoongEgg.Views;
using System;
using System.Windows;
namespace AppConsole
{
class Program
{
[STAThread]
static void Main(string[] args) {
//CalculatorViewModel viewModel = new CalculatorViewModel { Left = 111, Right = 222, Answer = 333 };
CalculatorView view = new CalculatorView { DataContext = viewModel };
Application app = new Application();
app.Run(view);
}
}
}
17. MVVM 없음
MainWindow.xaml
프런트 코드
<Window
x:Class="NoMVVM.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:local="clr-namespace:NoMVVM"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
FontSize="32"
mc:Ignorable="d">
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="AUTO" />
<ColumnDefinition />
<ColumnDefinition Width="AUTO" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- 左侧操作数 -->
<TextBox
x:Name="left"
Grid.Column="0"
VerticalAlignment="Center"
Text="666" />
<!-- 运算符们 -->
<StackPanel
Grid.Column="1"
VerticalAlignment="Center"
ButtonBase.Click="Button_Click">
<Button
Width="80"
Height="80"
Margin="5"
Content="+" />
<Button
Width="80"
Height="80"
Margin="5"
Content="-" />
<Button
Width="80"
Height="80"
Margin="5"
Content="*" />
<Button
Width="80"
Height="80"
Margin="5"
Content="/" />
</StackPanel>
<!-- 右侧操作数 -->
<TextBox
x:Name="right"
Grid.Column="2"
VerticalAlignment="Center"
Text="999" />
<!-- =号 -->
<Label
Grid.Column="3"
VerticalAlignment="Center"
Content="=" />
<TextBlock
x:Name="answer"
Grid.Column="4"
VerticalAlignment="Center"
Text="Answer" />
</Grid>
</Window>
MainWindow.xaml.cs
백엔드 코드
using System.Windows;
using System.Windows.Controls;
namespace NoMVVM
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow() {
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e) {
if(e.Source is Button btn) {
bool isDouble = false;
isDouble = double.TryParse( left.Text, out double leftOpr);
if (!isDouble) return;
isDouble = double.TryParse( right.Text, out double rightOpr);
if (!isDouble) return;
string opr = btn.Content.ToString();
switch (opr) {
case "+":answer.Text = (leftOpr + rightOpr).ToString(); break;
case "-":answer.Text = (leftOpr - rightOpr).ToString(); break;
case "*":answer.Text = (leftOpr * rightOpr).ToString(); break;
case "/":answer.Text = (leftOpr / rightOpr).ToString(); break;
default:
break;
}
}
}
}
}
18. 최초의 ViewModel 단순 계산기
- 계산기 ViewModel
using LoongEgg.LoongCore;
using System.Windows.Controls;
using System.Windows.Input;
namespace LoongEgg.ViewModels
{
// TODO: 18-1 计算器的ViewModel
/// <summary>
/// 计算器的ViewModel
/// </summary>
public class CalculatorViewModel: ViewModelBase
{
/*------------------------------------- Properties --------------------------------------*/ /// <summary>
/// 左侧操作数
/// </summary>
public int Left {
get => _Left;
set => SetProperty(ref _Left, value);
}
protected int _Left;
/// <summary>
/// 右侧操作数
/// </summary>
public int Right {
get => _Right;
set => SetProperty(ref _Right, value);
}
protected int _Right;
/// <summary>
/// 计算结果
/// </summary>
public int Answer {
get => _Answer;
set => SetProperty(ref _Answer, value);
}
protected int _Answer;
/// <summary>
/// 运算命令
/// </summary>
public ICommand OperationCommand { get; protected set; }
/*------------------------------------- Constructor -------------------------------------*/
/// <summary>
/// 默认构造器
/// </summary>
public CalculatorViewModel() {
OperationCommand = new DelegateCommand(Operation);
}
/*----------------------------------- Private Methods -----------------------------------*/
/// <summary>
/// 运算的具体执行方法
/// </summary>
/// <param name="opr"></param>
protected void Operation(object opr) {
var self = opr as Button;
switch (opr.ToString()) {
case "+": Answer = Left + Right; break;
case "-": Answer = Left - Right; break;
case "*": Answer = Left * Right; break;
case "/": Answer = Left / Right; break;
};
}
}
}
- CalculatorViewModel의 단위 테스트
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace LoongEgg.LoongCore.Test
{
// TODO: 15-2 DelegateCommand的单元测试
[TestClass]
public class DelegateCommand_Test
{
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Constructor_ThrowExeceptionIfExecuteParameterIsNULL() {
var command = new DelegateCommand(null);
}
[TestMethod]
public void Execute_CanInvokes() {
bool invoked = false;
var command = new DelegateCommand(
obj => { invoked = true; }
);
command.Execute(null);
Assert.IsTrue(invoked);
}
[TestMethod]
public void CanExecute_IsTrueByDefault() {
var command = new DelegateCommand(obj => { });
Assert.IsTrue( command.CanExecute(null));
}
[TestMethod]
public void CanExecute_TruePredicate() {
var command = new DelegateCommand
(
obj => { },
obj => (int)obj == 666
);
Assert.IsTrue(command.CanExecute(666));
}
[TestMethod]
public void CanExecute_FalsePredicate() {
var command = new DelegateCommand
(
obj => { },
obj => (int)obj == 666
);
Assert.IsFalse(command.CanExecute(66));
}
}
}
19. ViewModel과 View의 만남, Binding하기 전에 이 영상을 보시길 권장합니다.
https://www.bilibili.com/video/BV1ci4y1t7D6/
핵심
- 콘솔에서 WPF를 시작하고 DataContext를 ViewModel로 설정합니다.
- DesignModel은 ViewModel을 상속하지만 고유한 정적 속성을 가지고 있기 때문에 디자인 타임에 바인딩하여 오류 가능성을 줄일 수 있습니다.
- 명령 바인딩 CommandParameter를 바인딩하는 것을 잊지 마십시오(필요한 경우).
1. ViewModel 초기화 및 View 주입(의존성 주입)
using LoongEgg.ViewModels;
using LoongEgg.Views;
using System;
using System.Windows;
namespace AppConsole
{
class Program
{
[STAThread]
static void Main(string[] args) {
// TODO: 19-1 初始化ViewModel并注入View
// 初始化一个ViewModel并设置一些初始值以示和DesignModel不一样
CalculatorViewModel viewModel = new CalculatorViewModel { Left = 111, Right = 222, Answer = 333 };
// 将ViewModel赋值给View的DataContext
CalculatorView view = new CalculatorView { DataContext = viewModel };
Application app = new Application();
app.Run(view);
}
}
}
2. 디자인 타임 바인딩을 용이하게 하기 위해 DesignModel 생성
using LoongEgg.ViewModels;
namespace LoongEgg.Views
{
/*
|
| WeChat: InnerGeek
| [email protected]
|
*/
// TODO: 19-2 创建一个DesignModel以方便设计时绑定
public class CalculatorDesignModel: CalculatorViewModel
{
public static CalculatorDesignModel Instance => _Instance ?? (_Instance = new CalculatorDesignModel());
private static CalculatorDesignModel _Instance;
public CalculatorDesignModel() : base() {
Left = 999;
Right = 666;
Answer = 233;
}
}
}
3. Xaml에서 View와 ViewModel의 바인딩 완료
디자인 타임에 DataContext 바인딩을 설정하는 것을 잊지 마십시오 d:DataContext="{x:Static local:CalculatorDesignModel.Instance}"
. 오, 구문 힌트가 있습니다.
<Window
x:Class="LoongEgg.Views.CalculatorView"
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:local="clr-namespace:LoongEgg.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:LoongEgg.ViewModels;assembly=LoongEgg.ViewModels"
Title="Calculator View - 1st MVVM Application"
Width="800"
Height="450"
d:DataContext="{x:Static local:CalculatorDesignModel.Instance}"
FontSize="52"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="Width" Value="80" />
<Setter Property="Height" Value="80" />
<Setter Property="Margin" Value="5" />
</Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</Window.Resources>
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="auto" />
<ColumnDefinition />
<ColumnDefinition Width="auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- 左侧的操作数 -->
<TextBox Grid.Column="0" Text="{Binding Left}" />
<!-- 运算符们 -->
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<Button
Command="{Binding OperationCommand}"
CommandParameter="+"
Content="+" />
<Button
Command="{Binding OperationCommand}"
CommandParameter="-"
Content="-" />
<Button
Command="{Binding OperationCommand}"
CommandParameter="*"
Content="*" />
<Button
Command="{Binding OperationCommand}"
CommandParameter="/"
Content="/" />
</StackPanel>
<!-- 右侧操作数 -->
<TextBox Grid.Column="2" Text="{Binding Right}" />
<Label
Grid.Column="3"
VerticalAlignment="Center"
Content="=" />
<!-- 计算结果 -->
<TextBox Grid.Column="4" Text="{Binding Answer}" />
</Grid>
</Window>
20. 전체 네트워크에서 가장 간단한 IValueConverter 구현
1. IValueConverter 클래스의 일반적인 방법
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
/*
| 个人微信:InnerGeeker
| 联系邮箱:[email protected]
| 创建时间:2020/4/14 19:51:26
| 主要用途:
| 更改记录:
| 时间 版本 更改
*/
namespace LoongEgg.Views
{
/// <summary>
/// 整型转<see cref="Brush"/>
/// </summary>
public class IntToBrushConverter : IValueConverter
{
/*------------------------------------ Public Methods -----------------------------------*/
/// <summary>
/// 整数转<see cref="Brush"/><see cref="IValueConverter.Convert(object, Type, object, CultureInfo)"/>
/// </summary>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value == null) {
return null;
}else if((int) value < 18) {
return Brushes.Green;
}else {
return Brushes.Blue;
}
}
/// <summary>
/// 不重要
/// </summary>
public object ConvertBack
(
object value,
Type targetType,
object parameter,
CultureInfo culture
) => throw new NotImplementedException();
}
}
2. 일반적인 방법에서 IValueConverter 사용
- 정적 자원으로 정의
<Window.Resources>
<local:IntToBrushConverter x:Key="intToBrushConverter" />
</Window.Resources>
- 필요한 곳을 정적으로 참조
<!-- 左侧的操作数 -->
<TextBox
Grid.Column="0"
Foreground="{Binding Left, Converter={StaticResource intToBrushConverter}}"
Text="{Binding Left}" />
3. 모든 IValueConverter 기본 클래스에 한 번
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Windows.Markup;
/*
| 个人微信:InnerGeeker
| 联系邮箱:[email protected]
| 创建时间:2020/4/14 20:07:32
| 主要用途:
| 更改记录:
| 时间 版本 更改
*/
namespace LoongEgg.Views
{
/// <summary>
/// 值转换器的基类,是一个泛型方法,传入你要实现的值转换本身
/// </summary>
/// <typeparam name="T">你要的值转换器它本身类型</typeparam>
public abstract class BaseValueConverter <T>
: MarkupExtension, IValueConverter
where T: class, new()
{
/*---------------------------------------- Fields ---------------------------------------*/
/// <summary>
/// 值转换器的实例
/// </summary>
private static T _Instance;
/*------------------------------------ Public Methods -----------------------------------*/
/// <summary>
/// 为了在Xaml中直接使用<see cref="IValueConverter"/>必须实现的一个方法<see cref="MarkupExtension.ProvideValue(IServiceProvider)"/>
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns>返回值转换器的单实例</returns>
public override object ProvideValue(IServiceProvider serviceProvider)
=> _Instance ?? (_Instance = new T());
/// <summary>
/// <see cref="IValueConverter.Convert(object, Type, object, CultureInfo)"/>
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public abstract object Convert(object value, Type targetType, object parameter, CultureInfo culture);
/// <summary>
/// 将前台UI中的值转换给后台ViewModel一般用不上
/// </summary>
/// <param name="value">UI中的值</param>
/// <param name="targetType">目标类型</param>
/// <param name="parameter">额外的转换参数</param>
/// <param name="culture"></param>
/// <returns></returns>
public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}
4. 가장 간단한 ValueConverter 구현
using System;
using System.Globalization;
using System.Windows.Media;
/*
| 个人微信:InnerGeeker
| 联系邮箱:[email protected]
| 创建时间:2020/4/14 20:18:51
| 主要用途:
| 更改记录:
| 时间 版本 更改
*/
namespace LoongEgg.Views
{
/// <summary>
/// 最简单的值转换器实现
/// </summary>
public class AdvanceIntToBrushConverter : BaseValueConverter<AdvanceIntToBrushConverter>
{
/*------------------------------------ Public Methods -----------------------------------*/
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value == null) {
return null;
}else if( (int) value < 18) {
return Brushes.Green;
}else {
return Brushes.Yellow;
}
}
}
}
5. 가장 사용하기 쉬운 ValueConverter
<!-- 右侧操作数 -->
<TextBox
Grid.Column="2"
Background="{Binding Right, Converter={local:AdvanceIntToBrushConverter}}"
Text="{Binding Right}" />