构建现代化WPF应用:数据驱动开发与高级特性解析

启动时默认打开哪个界面是在App.xaml的StartupUri属性中设置。

Window标签

x:Class起到了部分类的作用,让XAML与CS文件做关联,起到了映射的作用。

xmlns是XML NameSpace的简称,起到了命名控件的作用,与代码中命名空间不同的是,它的值是一个URL,这样做的好处是可以将多个命名空间合并到一个URL里。

xmlns后面的字母是不同的命名空间的前缀,后续如果需要显示静态变量,自定义控件时会用到它。比如:Title="{x:Static local:FeedbackWindow.MyTitle}",将FeedbackWindow的MyTitle静态变量显示了出来;在使用评分控件时,需要在控件的名称前添加命名空间的前缀hc:<hc:Rate x:Name="myRate"></hc:Rate>

布局

StackPanel

StackPanel和Winform里的Panel很相似,默认是垂直布局,也可以水平局部,还可以互相嵌套。

Grid

Grid是窗体的根元素,它的作用和HTML的Table标签很相似,可以定义水平和垂直单元的个数,然后在各个单元中放置各个控件。

RowDefinitions- 像大楼一样,一层一层的堆叠,浏览网页就是水平展示的,从上到下显示

复制代码
<!-- 布局 -->
<Grid.RowDefinitions>
  <RowDefinition Height="0.2*"></RowDefinition>
  <RowDefinition Height="0.8*"></RowDefinition>
</Grid.RowDefinitions>

ColumnDefinitions - 像火车头一样,从左到右显示

复制代码
<!-- 水平布局 -->
<Grid.ColumnDefinitions>
  <ColumnDefinition Width="0.35*"></ColumnDefinition>
  <ColumnDefinition Width="0.65*"></ColumnDefinition>
</Grid.ColumnDefinitions>

值得注意的是,XAML与HTML的区别之处还在于,在使用了Grid之后,在将其他元素放到Grid中时不是直接放到它的内部,而是外部,并通过Grid.Column/Row=Num来标记这个内容是放在哪儿。

比如在定义一个Border控件的时候,需要在控件的标签里添加这个属性:

复制代码
<!-- 右侧背景色 -->
<Border Grid.Column="1" CornerRadius="5" Background="WhiteSmoke">
</Border>

这是在Grid的第2列单元格中定义了一个Border,背景色是白色。

资源

在编写XAML的时候会发现在许多控件中有着重复的属性,使用资源标签就可以将它们抽离出来,合并到一起,起到简化代码的作用。它类似于HTML中的CSS,将同一类型控件的样式放到<style>标签中,使用的时候告诉控件指定样式的名字即可。

1、定义资源

可以在3个不同的地方定义资源:在当前界面中,在程序入口文件中,在资源文件中。

在当前页面中定义:

复制代码
<Window.Resources>
  <SolidColorBrush x:Key="myColor1" Color="Red"></SolidColorBrush>
</Window.Resources>

定义资源需要用到Key属性,相当于HTML标签的id属性,是用来标记的,标签名称SolidColorBrush是控件类型,Color=Red是属性的键值对。

在程序入口文件App.xaml中定义:

复制代码
<Application.Resources>
  <ResourceDictionary>
    <!-- 定义资源 -->
    <SolidColorBrush x:Key="myColor2" Color="Orange"></SolidColorBrush>
  </ResourceDictionary>
</Application.Resources>

在资源文件中定义:

首先我们要新建一个资源字典,然后在里面定义资源

复制代码
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <SolidColorBrush x:Key="myColor3" Color="Yellow"></SolidColorBrush>
</ResourceDictionary>

同时还得在App.xaml中引入该资源文件:

复制代码
<Application.Resources>
  <ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
      <!-- 引入外部资源文件 -->
      <ResourceDictionary Source="/ColorDictionary.xaml"/>
    </ResourceDictionary.MergedDictionaries>
  </ResourceDictionary>
</Application.Resources>

2、使用资源

使用资源比声明要简单一些,只需在需要用到的属性的值里面添加{},并在里面输入StaticResource或DynamicResource,再输入一个控件,后面再跟上资源的Key值即可。

复制代码
<Border Grid.Row="1" Grid.Column="0" Background="{StaticResource myColor3}"></Border>
<Border Grid.Row="1" Grid.Column="1" Background="{DynamicResource myColor4}"></Border>

StaticResource和DynamicResource的区别在于一个不能修改,一个可以修改。

一般修改的逻辑会写在cs文件中:

复制代码
private void Button_Click(object sender, RoutedEventArgs e)
{
    // 将资源进行修改,可以看到只有动态资源可以修改,静态的不行,这个在一些需要切换状态的场景中会用到
    Resources["myColor4"] = new SolidColorBrush(Color.FromRgb(30, 70, 120));
    Resources["myColor3"] = new SolidColorBrush(Color.FromRgb(30, 70, 120));
}

使用Resource[资源的Key值] = 目标值,即可完成修改。

样式

介绍完了资源之后,就不得不提到样式,样式是把资源集合化了,它可以包含多个样式。

在当前页面中定义样式:

复制代码
<Window.Resources>
  <Style x:Key="buttonStyle" TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}">
    <Setter Property="Width" Value="180"></Setter>
    <Setter Property="Height" Value="35"></Setter>
    <Setter Property="FontSize" Value="18"></Setter>
    <Setter Property="Foreground" Value="White"></Setter>
  </Style>
</Window.Resources>

应用样式:

复制代码
<Button Margin="0 50 0 0" Background="Transparent" Style="{StaticResource buttonStyle}">注册</Button>

最初在应用样式的时候发现一个问题,就是明明没有修改按钮的背景色,悬浮背景色以及边框,但是它却自动被修改了,问了下AI原来是系统的默认行为,想要只应用Setter中的样式,需要在添加 BasedOn="{StaticResource {x:Type Button}}去继承Button的样式。

控件模板

控件模板顾名思义就是将多个具有相似之处的模板抽离成一个模板,比如一个表单中,最常见的一个模板就是一个文本+输入框。

它也是一种资源,所以需要像前面的样式一样去定义一个资源然后使用它

1、定义模板

复制代码
<Window.Resources>
  <ControlTemplate x:Key="myTemplate" TargetType="{x:Type TextBox}">
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top">
      <TextBlock Margin="0 10" Text="{TemplateBinding Tag}"></TextBlock>
      <TextBox Height="33" Width="200">
      </TextBox>
    </StackPanel>
  </ControlTemplate>
</Window.Resources>

如果要使用模板,需要使用ControlTemplate标签,Key是唯一标识,TargetType是使用模板的控件的类型。标签内部就是模板体了,里面是一个文本和一个输入框,输入框的Text值是从使用模板的属性Tag中去取。

2、使用模板

复制代码
<StackPanel>
  <TextBox Height="33" Width="400" Tag="用户名: " Template="{ }">
  </TextBox>
</StackPanel>

在使用模板时,需要定义和模板的TargetType相同类型的控件,然后使用Template属性,里面的值和上一节使用样式时一样,在{}中写【资源类型】【资源名称】。

数据绑定

绑定基础

数据绑定是MVVM,模型的核心之一,它与Winform中以事件驱动的模型不同,实现了在界面里控件的属性(官方说法称之为特征值)发生改变后,会自动影响到实现了Model类的实例的属性(官方说法称之为源)。

绑定方向有双向绑定,顾名思义就是控件或者类有一个发生改变都会立即同步;还有一种是单向绑定,有两个方向,控件发生改变会通知类,但类改变不会影响控件,亦或者反过来;最后一种是只绑定一次,就是在首次加载时。

另外还有绑定的触发时机也分两种,一种是控件属性发生改变后立即生效,一种是控件失去焦点时生效。

实现绑定还有一个前提是需要类实现INotifyPropertyChanged接口,并在属性的Setter方法里调用它。

1、生成一个类,并实现INotifyPropertyChanged,修改默认的Setter方法,代码块:propfull。

复制代码
public class Employee : INotifyPropertyChanged
{
    private string name;

    public string Name
    {
        get { return name; }
        set { 
            name = value; 
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}

2、在后台cs文件里将类与控件绑定,这个是在初始化控件完成后进行的

复制代码
// 开始绑定,首先声明一个绑定对象,可以给它设置类的源头和要绑定到哪个属性上
var binding = new Binding();
binding.Source = _employee;
binding.Path = new PropertyPath("Name");

// 设置绑定的方向,是双向绑定(默认)- TwoWay,单向绑定-OneWay,OneWayToSource,还有只绑定一次-OneTime,单向绑定又分为两种,从类绑定到界面-OneWay;从界面绑定到类-OneWayToSource
binding.Mode = BindingMode.Default;

// 设置绑定的触发器,默认是PropertyChanged,但这种方式在应用在输入框时更新过于频繁,推荐设置为LostFocus,在失去焦点时更新
binding.UpdateSourceTrigger = UpdateSourceTrigger.Default;

// 最后是设置控件的属性,并将它与类的关联起来
this.EmployeeName.SetBinding(TextBox.TextProperty, binding);

控件间的绑定

控件间的绑定就是一个控件的属性发生改变后,立即影响另外一个控件,只需在XAML中编写相关代码。

在控件的特征值里指定源和属性,比如下面的例子中是在一个TextBox控件的Text属性中指定名为HeightSlider控件的Value属性。

复制代码
<!-- 将滑动控件的Value值绑定到TextBox的Text属性里 -->
<TextBox x:Name="TextBoxHeight" BorderBrush="Black"  Height="50" 
  Text="{Binding Path=Value, ElementName=HeightSlider}"/>
<Slider Name="HeightSlider" Height="50" Margin="0 20 0 0" Minimum="0" Maximum="100" IsSnapToTickEnabled="True"></Slider>

数据上下文绑定

数据上下文绑定(DataContext Binding)也是在XAML中就可以完成的绑定操作。它是在Window.DataContext标签中声明源,属性,属性值。

复制代码
<Window x:Class="XCYN.NET8.WPF.ControlBindWindow"
  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:local="clr-namespace:XCYN.NET8.WPF"
  mc:Ignorable="d"
  Title="ControlBindWindow" Height="450" Width="800">
<Window.DataContext>
  <model:Employee Name="张三"></model:Employee>
</Window.DataContext>
<Grid>
  <!-- 通过DataContext的方式进行绑定,这种方式不用再后台编写代码,需要声明一个Window.DataContext标签,在里面声明源和特征值 -->
  <TextBox Text="{Binding Name}" BorderBrush="Black" Height="44"></TextBox>
</Grid>

列表绑定

列表绑定是最常用的绑定方式了,简单的说就是将一个List数组绑定到列表控件中,类似于Winform中的DataGridView。

它有两种方式,显示指定DisplayMemberPath和不显示指定,但需要额外编写<ListBox.ItemTemplate>和<DataTemplate>标签。

第一种方式使用起来比较简单,相对的可以做的更少一些,可以用于简单的列表展示;第二种要复杂一些,但也能做更复杂的操作。

第一种方式

声明一个ListBox控件,在CodeBehind生成数据源

复制代码
<ListBox Height="300" x:Name="ListBoxEmployee"></ListBox>

    List<Employee> list = new List<Employee>()
    {
        new Employee()
        {
            Id = "1",
            Name = "张三",
            Address = "湖北省武汉市"
        },
        new Employee()
        {
            Id = "2",
            Name = "李四",
            Address = "湖南省长沙市"
        },
        new Employee()
        {
            Id = "3",
            Name = "王五",
            Address = "广东省东莞市"
        }
    };
    
    this.ListBoxEmployee.ItemsSource = list;
    this.ListBoxEmployee.DisplayMemberPath = "Name";

这里完成了对ListBox的数据绑定和属性的指定,这里指定的是Name属性,待会会显示在列表中。

第二种方式

声明一个ListBox控件,在CodeBehind生成数据源,在ListBox控件里声明ListBox.ItemTemplate标签,在这个标签里再声明DataTemplate标签,最后在这里关联源的属性。

复制代码
  <ListBox Height="300" x:Name="ListBoxEmployee">
    <ListBox.ItemTemplate>
      <DataTemplate>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="{Binding Path=Id}" Width="50" Margin="5"></TextBlock>
          <TextBlock Text="{Binding Path=Name}"  Width="100" Margin="5"></TextBlock>
          <TextBlock Text="{Binding Path=Address}"  Width="150" Margin="5"></TextBlock>
        </StackPanel>
      </DataTemplate>
    </ListBox.ItemTemplate>
  </ListBox>

这里绑定了三个TextBlock,分别指定ID,Name,Address属性。

复制代码
    List<Employee> list = new List<Employee>()
    {
        new Employee()
        {
            Id = "1",
            Name = "张三",
            Address = "湖北省武汉市"
        },
        new Employee()
        {
            Id = "2",
            Name = "李四",
            Address = "湖南省长沙市"
        },
        new Employee()
        {
            Id = "3",
            Name = "王五",
            Address = "广东省东莞市"
        }
    };
    
    this.ListBoxEmployee.ItemsSource = list;

相对绑定

自身绑定

自身绑定是以控件自身既作为源也作为控件去使用源的属性。

复制代码
<Button Width="100" Content="登录" FontSize="18" Height="{Binding RelativeSource={
  RelativeSource Mode=Self
  }, Path=Width}"></Button>

上面的例子是以Width为源的特征值,显示在Height属性中,需要使用RelativeSource并指定它的Mode为Self。

父级绑定

父级绑定是以控件的父级为源,将里面的特征值显示在子级控件里。

复制代码
<Grid Background="LightPink">
    <Grid Background="AliceBlue" Height="300" Width="500">
        <StackPanel>
            <Button Width="100" Content="登出" FontSize="18" Height="100" Background="{Binding RelativeSource={
                RelativeSource Mode=FindAncestor,
                AncestorLevel=2,
                AncestorType=Grid
                }, Path=Background}">
            </Button>
        </StackPanel>
    </Grid>
</Grid>

需要使用RelativeSource并指定它的Mode为FindAncestor,同时也要指定层级关系AncestorLevel,控件类型AncestorType。

转换器

转换器的作用是将无法直接显示在属性里的值,比如枚举类型,转换为可以在属性里显示的类型。

首先我们要生成一个转换器的类并继承IValueConverter接口,Convert方法是正向转换,ConvertBack是反向转换,这里只实现了正向转换,将JobIcon枚举类型转为字符串类型。

复制代码
    class JobIconConvert : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var obj = (JobIcon)value;
            switch (obj)
            {
                case JobIcon.ZS:
                    return "Image/战士.png";
                case JobIcon.MS:
                    return "Image/牧师.png";
                case JobIcon.FS:
                    return "Image/法师.png";
                case JobIcon.XD:
                    return "Image/小德.png";
                case JobIcon.QS:
                    return "Image/骑士.png";
                default:
                    return null;
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

在XAML中需要在资源里声明该转换器,然后在用到这个枚举类型的属性里使用它

复制代码
<Window.Resources>
  <model:JobIconConvert x:Key="jobConvert"></model:JobIconConvert>
</Window.Resources>
<Grid>
  <StackPanel>
    <TextBlock Text="请选择你的职业" Height="50" FontSize="28"></TextBlock>
    <ListBox x:Name="listBox_zy" Height="300">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding Path=Name}" Margin="5"></TextBlock>
            <Image Margin="5" Source="{Binding Path=Icon,Converter={StaticResource jobConvert}}"></Image>
          </StackPanel>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </StackPanel>
</Grid>

在CodeBehind中只是简单地初始化源和进行数据绑定。

复制代码
    list = new List<WowJob>()
    {
        new WowJob(){Name = "战士", Icon = JobIcon.ZS},
        new WowJob(){Name = "牧师", Icon = JobIcon.MS},
        new WowJob(){Name = "法师", Icon = JobIcon.FS},
        new WowJob(){Name = "小德", Icon = JobIcon.XD},
        new WowJob(){Name = "骑士", Icon = JobIcon.QS},
    };
    
    this.listBox_zy.ItemsSource = this.list;

多路绑定

多路绑定是将多个源绑定到一个控件中,比如在注册用户名时,需要输入两次密码,才能通过验证,实现它和转换器类似,需要一个实现了IMultiValueConverter接口的类去做验证。

复制代码
    class PasswordConvert : IMultiValueConverter
    {
        /// <summary>
        /// 验证两个密码是否相同.
        /// </summary>
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            var arr = Array.ConvertAll(values, m => m.ToString());
            if (arr.Length == 2 &&
                arr[0] == arr[1] &&
                arr[0].Length > 0)
            {
                return true;
            }

            return false;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

编写XAML,新建两个输入框和按钮,按钮默认是无法使用的状态,当两个密码是相同的时变为可用。

复制代码
<StackPanel>
  <StackPanel Orientation="Horizontal" Margin="5">
    <TextBlock Text="密码" Width="50" Margin="5"></TextBlock>
    <TextBox Name="txt_pwd1" Width="200"></TextBox>
  </StackPanel>
  <StackPanel Orientation="Horizontal" Margin="5">
    <TextBlock Text="新密码" Width="50" Margin="5"></TextBlock>
    <TextBox Name="txt_pwd2" Width="200"></TextBox>
  </StackPanel>
  <Button x:Name="btn_register" Content="注册" IsEnabled="False" HorizontalAlignment="Left" Margin="5"></Button>
</StackPanel>

最后是在CodeBehind中,进行多路绑定,有点小复杂,首先以两个输入框作为源,里面的Text属性作为Path,将它们添加到MultiBinding中,给这个实例设置为单向绑定,给它设置转换器,就是刚才新建的,最后是给按钮控件设置绑定的属性和绑定实例。

复制代码
    // 生成控件的绑定对象,这里是把它们作为源了,然后把它们添加到多组绑定对象中,并设置它们的转换器,在转换器内进行数据验证
    var binding1 = new Binding("Text")
    {
        Source = this.txt_pwd1
    };
    var binding2 = new Binding("Text")
    {
        Source = this.txt_pwd2
    };
    var multiBinding = new MultiBinding()
    {
        Mode = BindingMode.OneWay
    };
    multiBinding.Bindings.Add(binding1);
    multiBinding.Bindings.Add(binding2);
    multiBinding.Converter = new PasswordConvert();

    // 最后给目标控件设置要控制的属性和绑定对象
    this.btn_register.SetBinding(Button.IsEnabledProperty, multiBinding);

依赖属性

依赖属性是在普通属性的基础上,让类继承自DependencyObject,可以通过propdp代码块生成的该属性片段。由于它是通过类似单例模式的方式去注册对象,所以起到了节省资源的效果,比如:如果创建多个依赖对象,它们未显示指定的一个属性值是会被共享的,这样就节省了内存的开销;并且所有实例都是共享同一套元数据(如默认值,回调函数等),而不是每个单独的实例去存储这些;当依赖属性不再使用时,WPF会自动清除相关资源,避免内存泄漏。

在WPF中所有的UI控件都是依赖对象,依赖属性默认是支持绑定的,无需再去继承INotifyPropertyChanged去实现绑定。

依赖属性是通过HashTable去维护对象,这种数据结构是一种"以时间换空间"的结构。

1、创建依赖对象

复制代码
    class Student : DependencyObject
    {
        public int Age
        {
            get { return (int)GetValue(AgeProperty); }
            set { SetValue(AgeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Age.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AgeProperty =
            DependencyProperty.Register("Age", typeof(int), typeof(Student), new PropertyMetadata(0));
    }

2、将依赖对象绑定到控件中,可以看到它是支持绑定的,在界面中显示了18

复制代码
    var binding = new Binding();
    var student = new Student() { Age = 18 };
    binding.Source = student;
    binding.Path = new PropertyPath("Age");
    this.myButton.SetBinding(Button.ContentProperty, binding);

附加属性

附加属性经常会用到,在之前使用Grid进行布局时就用到过,在StackPanel中就用到了Grid.Column来定位控件,在自定义时同样需要继承自DependencyObject,并通过propa来快速生成该属性。

复制代码
    class School : DependencyObject
    {
        public static string GetGrade(DependencyObject obj)
        {
            return (string)obj.GetValue(MyGradeProperty);
        }

        public static void SetGrade(DependencyObject obj, string value)
        {
            obj.SetValue(MyGradeProperty, value);
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MyGradeProperty =
            DependencyProperty.RegisterAttached("Grade", typeof(string), typeof(School), new PropertyMetadata(""));
    }

这里定义了一个School的Grade附加属性,接下来将把这个属性附加到Student中,这样就可以做到不改变Student的情况下,让它可以使用Grade这个属性,实现了开闭原则。

复制代码
    // 使用自定义的附加属性去设置和获取值
    Student student = new Student()
    {
        Age = 18,
    };
    School.SetGrade(student, "一年级");
    var grade = School.GetGrade(student);

下面是使用WPF的控件的附加属性,实现拖动滑块后让矩形块上下左右移动的代码片段。

复制代码
<StackPanel>
  <Slider x:Name="xLocationSlider" Margin="5" Minimum="0" Maximum="200"></Slider>
  <Slider x:Name="yLocationSlider"  Margin="5" Minimum="0" Maximum="200"></Slider>
  <Canvas>
    <!-- Rectangle位于Canvas画布的内部,就可以使用它的附加属性,然后通过绑定对附加属性进行控制 -->
    <Rectangle Margin="5" Width="50" Height="50" Fill="BlueViolet" Canvas.Left="{Binding ElementName=xLocationSlider, Path=Value}" Canvas.Top="{Binding ElementName=yLocationSlider, Path=Value}"></Rectangle>
  </Canvas>
</StackPanel>

路由事件

路由事件是一种消息接收方法,它不同于Winform那种点对点的事件触发,它是在一个控件触发事件后,将事件不断地往外层传递,直到有钩子接收到该事件。

首先我们要给钩子的控件取个名字root,然后给发出事件的控件取个名字。

复制代码
<Grid x:Name="root">
  <StackPanel>
    <Button x:Name="btn_submit" Content="提交" Margin="5"></Button>
  </StackPanel>
</Grid>

接着在CodeBehind中,在控件初始化完毕后注册一个钩子,这里是给Button的Click注册了一个事件,接收方是root。

复制代码
    // 通过对最外层的Grid控件调用AddHandler方法完成事件的注册,第一个参数不同类型的控件的事件,这里是Button按钮的点击事件
    // 第二个参数是事件触发时调用的委托,这里使用lambda表达式来定义委托,委托中可以获取到触发事件的控件
    this.root.AddHandler(Button.ClickEvent, new RoutedEventHandler((m, n) => {
        // OriginalSource属性可以获取到触发事件的控件
        var element = n.OriginalSource as FrameworkElement;
        MessageBox.Show(element?.Name);
    
        // 也可以将sender参数进行转换,获取接收事件的控件
        var sender = m as FrameworkElement;
        MessageBox.Show(sender?.Name);
    }));

路由命令

路由命令比起路由事件更加具体,因为它显示的指定了绑定的控件名称,因此更好控制,不过使用起来更加复杂。

首先在XAML中创建控件

复制代码
<Grid x:Name="root">
    <StackPanel>
        <TextBox x:Name="txt_name" Width="200" Height="50" Margin="5"></TextBox>
        <Button x:Name="btn_clear" Width="100" Height="40" Margin="5" Content="清除"></Button>
    </StackPanel>
</Grid>

然后在CodeBehind中声明一个命令,并将命令的名称和当前类的类型传递过去

复制代码
/// <summary>
/// 声明一个路由命令,第一个参数是命令的名称,第二个参数是该命令属于哪个类.
/// </summary>
private RoutedCommand command = new RoutedCommand("myEvent", typeof(RoutedCommandWindow));

最后是将该命令绑定到控件中

复制代码
    // 为命令指定源和目标
    this.btn_clear.Command = this.command;
    this.btn_clear.CommandTarget = this.txt_name;
    
    // 创建命令的Binding
    var binding = new CommandBinding();
    binding.Command = this.command;
    binding.CanExecute += (m, n) =>
    {
        // 是否执行命令
        if (this.txt_name.Text.Length > 0)
        {
            n.CanExecute = true;
        }
        else
        {
            n.CanExecute = false;
        }
    
        n.Handled = true;
    };
    binding.Executed += (m, n) =>
    {
        // 如何执行命令
        this.txt_name.Text = "";
        n.Handled = true;
    };
    
    // 何处接收命令,一般在上层元素中
    this.root.CommandBindings.Add(binding);
相关推荐
码观天工1 小时前
【.NET必读】RabbitMQ 4.0+重大变更!C#开发者必须掌握的6大升级要点
c#·rabbitmq·.net·mq
o0向阳而生0o4 小时前
43、Server.UrlEncode、HttpUtility.UrlDecode的区别?
c#·.net
敲代码的 蜡笔小新4 小时前
【行为型之策略模式】游戏开发实战——Unity灵活算法架构的核心实现策略
unity·设计模式·c#·策略模式
Kookoos4 小时前
【实战】基于 ABP vNext 构建高可用 S7 协议采集平台(西门子 PLC 通信全流程)
后端·物联网·c#·.net
钢铁男儿7 小时前
C# 方法(栈帧)
开发语言·c#
码小跳13 小时前
Halcon案例(一):C#联合Halcon识别路由器上的散热孔
图像处理·c#
神仙别闹15 小时前
基于C#+MySQL实现(WinForm)企业设备使用信息管理系统
开发语言·mysql·c#
czhaii16 小时前
PLC脉冲位置 单片机跟踪读取记录显示
开发语言·c#
神仙别闹17 小时前
基于C#+SQL Server开发(WinForm)租房管理系统
数据库·oracle·c#