44、WPF 数据绑定实用指南

WPF 数据绑定实用指南

在 WPF(Windows Presentation Foundation)开发中,数据绑定是一项核心技术,它能够实现数据与界面的分离,提高代码的可维护性和可测试性。本文将详细介绍 WPF 中的数据绑定,包括命令绑定、简单数据绑定、值转换、多属性绑定以及列表绑定等内容。

1. 命令绑定基础

在 WPF 中,命令触发(比如按钮点击)时的操作并非由命令本身定义,而是通过命令绑定将命令与事件处理程序关联起来。例如,在以下代码中, Window CommandBindings 定义了如果“Open”命令被触发,将调用 OnOpen 方法。

<Window.CommandBindings>
    <CommandBinding Command="Open" Executed="OnOpen" />
</Window.CommandBindings>

命令绑定会在层次结构中进行搜索, CommandBindings 属性定义在 UIElement 类中,而 UIElement 是每个 WPF 元素的基类。这样,控件可以定义命令绑定并实现处理程序,只需定义命令源即可。例如, TextBox 类实现了“Cut”、“Copy”、“Paste”和“Undo”命令的处理程序,只需为这些命令定义命令源(如 MenuItem 元素)。

1.1 使用 MVVM 和 DelegateCommand

在 MVVM(Model-View-ViewModel)模式中,直接在 XAML 文件中进行命令绑定可能不太合适,因为这种方式会导致与命令目标的紧密耦合。在 MVVM 中,命令和处理程序由 ViewModel 类定义,通过绑定命令实现松散耦合。
为了实现这一点,需要实现 ICommand 接口,以下是 DelegateCommand 类的实现:

using System;
using System.Windows.Input;

namespace Formula1.Infrastructure
{
    public class DelegateCommand : ICommand
    {
        private readonly Action<object> execute;
        private readonly Func<object, bool> canExecute;

        public DelegateCommand(Action<object> execute)
            : this(execute, null)
        {
        }

        public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            return canExecute == null ? true : canExecute(parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add
            {
                CommandManager.RequerySuggested += value;
            }
            remove
            {
                CommandManager.RequerySuggested -= value;
            }
        }

        public void Execute(object parameter)
        {
            execute(parameter);
        }
    }
}

这种 DelegateCommand 类是大多数 MVVM 框架的一部分。

2. 创建 ViewModel

以一个 Formula 1 应用程序为例,展示如何在用户界面上显示 Racer 类型的值。该示例通过命令查询特定名称的赛车手。

2.1 定义 ViewModel 类

ShowRacerViewModel 类派生自 ViewModelBase ,并定义了一个命令 FindRacerCommand

using System.Data;
using System.Data.Objects;
using System.Linq;
using Formula1.Infrastructure;
using Formula1.Model;

namespace Formula1.ViewModels
{
    public class ShowRacerViewModel : ViewModelBase
    {
        private DelegateCommand findRacerCommand;
        public DelegateCommand FindRacerCommand
        {
            get
            {
                return findRacerCommand ??
                    (findRacerCommand = new DelegateCommand(
                        param => this.FindRacer(param)));
            }
        }

        public void FindRacer(object name)
        {
            try
            {
                string filter = (string)name;
                using (Formula1Entities data = new Formula1Entities())
                {
                    var q = (from r in data.Racers
                             where r.LastName.StartsWith(filter)
                             select r);

                    Racer = (q as ObjectQuery<Racer>).
                            Execute(MergeOption.NoTracking).FirstOrDefault();
                }
            }
            catch (EntityException)
            {
                SetError("Verify the database connection");
            }
        }

        private Racer racer;
        public Racer Racer
        {
            get { return racer; }
            set 
            {
                if (!object.Equals(racer, value))
                { 
                    racer = value;
                    RaisePropertyChanged("Racer");
                }
            }
        }
    }
}

2.2 命令绑定流程

  • 在 XAML 代码中,定义一个按钮,将其 Command 属性绑定到 ViewModel 类的 FindRacerCommand 属性:
<TextBox x:Name="textName" Grid.Row="0" Grid.Column="0" 
         Margin="5" VerticalAlignment="Center" />
<Button Grid.Row="0" Grid.Column="1" Content="Find Racer" Margin="5" 
        Command="{Binding FindRacerCommand}"
        CommandParameter="{Binding ElementName=textName, Path=Text, 
             Mode=OneWay}" />
  • 当按钮被点击时, FindRacer 方法被调用,该方法使用 ADO.NET EF 进行查询,并将结果赋值给 Racer 属性。

3. 使用简单数据绑定

为了显示赛车手的信息,创建多个 TextBlock 元素,将其 Text 属性绑定到 Nationality Starts Wins 等属性。这些控件位于一个 Grid 控件中, Grid DataContext 设置为绑定到 ViewModel Racer 属性。

<UserControl x:Class="Formula1.Views.ShowRacerView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-
                  compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:utils="clr-namespace:Formula1.Infrastructure"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <Style TargetType="TextBlock">
            <Style.Setters>
                <Setter Property="VerticalAlignment" Value="Center" />
                <Setter Property="Margin" Value="10,7,5,7" />
            </Style.Setters>
        </Style>
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <TextBox x:Name="textName" Grid.Row="0" Grid.Column="0" 
                 Margin="5" VerticalAlignment="Center" />
        <Button Grid.Row="0" Grid.Column="1" Content="Find Racer" Margin="5" 
                Command="{Binding FindRacerCommand}"
                CommandParameter="{Binding ElementName=textName, Path=Text, 
                     Mode=OneWay}" />

        <Grid Grid.Row="1" Grid.ColumnSpan="2" DataContext="{Binding Racer}">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>

            <TextBlock Text="Name" Grid.Row="0" Grid.Column="0" />
            <TextBlock Grid.Row="0" Grid.Column="1" />
            <TextBlock Text="Country" Grid.Row="1" Grid.Column="0" />
            <TextBlock Grid.Row="1" Grid.Column="1" 
                       Text="{Binding Nationality}" />
            <TextBlock Text="Starts" Grid.Row="2" Grid.Column="0" />
            <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Starts}" />
            <TextBlock Text="Wins" Grid.Row="3" Grid.Column="0" />
            <TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding Wins}" />
            <TextBlock Text="Points" Grid.Row="4" Grid.Column="0" />
            <TextBlock Grid.Row="4" Grid.Column="1" Text="{Binding Points}" />
        </Grid>
        <TextBlock Foreground="Red" Text="{Binding ErrorMessage}" Grid.Row="2" 
                   Grid.Column="0" Grid.ColumnSpan="2" />
    </Grid>
</UserControl>

3.1 简单数据绑定步骤

  1. 创建 TextBlock 元素用于显示信息。
  2. TextBlock Text 属性绑定到相应的属性。
  3. 设置 Grid DataContext ViewModel Racer 属性。

4. 值转换

为了仅在出现错误时显示错误信息的 TextBlock ,可以将 TextBlock Visibility 属性绑定到 ViewModelBase 类的 HasError 属性。由于 HasError bool 类型,而 Visibility 是枚举类型,需要使用转换器进行绑定。

4.1 实现转换器

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace Formula1.Infrastructure
{
    [ValueConversion(typeof(bool), typeof(Visibility))]
    public class VisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, 
                              object parameter, CultureInfo culture)
        {
            bool input = (bool)value;
            if (input)
                return Visibility.Visible;
            else
                return Visibility.Collapsed;

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

4.2 在 XAML 中使用转换器

<UserControl.Resources>
    <utils:VisibilityConverter x:Key="VisibilityConverter" />
</UserControl.Resources>
<TextBlock Foreground="Red" Visibility="{Binding HasError, 
    Converter={StaticResource VisibilityConverter}}" 
    Text="{Binding ErrorMessage}" Grid.Row="2" Grid.Column="0" 
    Grid.ColumnSpan="2" />

4.3 值转换步骤

  1. 实现 IValueConverter 接口的转换器类。
  2. 在 XAML 资源中定义转换器。
  3. 在绑定中使用转换器。

5. 多属性绑定

如果需要将源的多个属性绑定到一个 UI 元素,可以使用 MultiBinding 。例如,将 TextBlock Text 属性绑定到 Racer FirstName LastName 属性。

5.1 使用 MultiBinding

<TextBlock Grid.Row="0" Grid.Column="1">
    <TextBlock.Text>
        <MultiBinding Converter="{StaticResource NameConverter}">
            <Binding Path="FirstName" />
            <Binding Path="LastName" /> 
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

5.2 实现转换器

using System;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;

namespace Formula1.Infrastructure
{
    public class NameConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, 
                              object parameter, CultureInfo culture)
        {
            if (values == null || values.Count() != 2)
                return DependencyProperty.UnsetValue;
            return String.Format("{0} {1}", values[0], values[1]);
        }

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

5.3 多属性绑定步骤

  1. MultiBinding 中定义要绑定的属性。
  2. 实现 IMultiValueConverter 接口的转换器类。
  3. MultiBinding 中指定转换器。

6. 列表绑定

接下来,将绑定赛车手列表。在 ShowRacersView 视图和 ShowRacersViewModel 视图模型中实现。

6.1 定义 ViewModel 类

using System;
using System.Collections.Generic;
using System.Linq;
using Formula1.Infrastructure;
using Formula1.Model;

namespace Formula1.ViewModels
{
    public class ShowRacersViewModel : ViewModelBase, IDisposable
    {
        private Formula1Entities data;
        public ShowRacersViewModel()
        {
            if (!IsDesignTime)
            {
                data = new Formula1Entities();
            }            
        }
        private DelegateCommand getRacersCommand;
        public DelegateCommand GetRacersCommand
        {
            get
            {
                return getRacersCommand ??
                    (getRacersCommand = new DelegateCommand(
                        param => this.GetRacers()));
            }
        }

        public bool FilterCountry { get; set; }
        public bool FilterYears { get; set; }

        private string[] countries;
        public IEnumerable<string> Countries
        {
            get
            {
                return countries ?? 
                    (countries = data.Racers.Select(
                        r => r.Nationality).Distinct().ToArray());
            }
        }
        public string SelectedCountry { get; set; }

        private int minYear;
        public int MinYear
        {
            get
            {
                if (IsDesignTime)
                    minYear = 1950;
                return minYear != 0 ? minYear : minYear = 
                    data.Races.Select(r => r.Date.Year).Min();
            }
        }

        private int maxYear;
        public int MaxYear
        {
            get
            {
                if (IsDesignTime)
                    maxYear = DateTime.Today.Year;
                return maxYear != 0 ? maxYear : maxYear = 
                    data.Races.Select(r => r.Date.Year).Max();
            }
        }

        private int selectedMinYear;
        public int SelectedMinYear
        {
            get
            {
                return selectedMinYear;
            }
            set
            {
                if (!object.Equals(selectedMinYear, value))
                {
                    selectedMinYear = value;
                    RaisePropertyChanged("SelectedMinYear");
                }
            }
        }
        private int selectedMaxYear;
        public int SelectedMaxYear
        {
            get
            {
                return selectedMaxYear;
            }
            set
            {
                if (!object.Equals(selectedMaxYear, value))
                {
                    selectedMaxYear = value;
                    RaisePropertyChanged("SelectedMaxYear");
                }
            }
        }

        public void Dispose()
        {
            data.Dispose();
        }

        private void GetRacers()
        {
            RaisePropertyChanged("Racers");
        }

        private IQueryable<Racer> GetExpression()
        {
            var expr = data.Racers as IQueryable<Racer>;
            if (FilterCountry)
            {
                expr = expr.Where(r => r.Nationality == this.SelectedCountry);
            }
            if (FilterYears)
            {
                expr = expr.SelectMany(
                    r => r.RaceResults,
                    (r1, raceResult) => new { Racer = r1, RaceResult = 
                         raceResult })
                    .Where(raceInfo => 
                        raceInfo.RaceResult.Race.Date.Year >= SelectedMinYear &&
                        raceInfo.RaceResult.Race.Date.Year <= SelectedMaxYear)
                    .Select(raceInfo => raceInfo.Racer)
                    .Distinct();
            }                    
            return expr;                    
        }
    }
}

6.2 在 XAML 中实现 UI

<GroupBox Header="Filter" Grid.Row="0">
    <Grid Grid.Row="0">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <CheckBox IsChecked="{Binding FilterCountry}" Content="Country"
                  Grid.Row="0" Grid.Column="0" Margin="5" />
        <ComboBox ItemsSource="{Binding Countries}" 
                  SelectedItem="{Binding SelectedCountry, 
                      Mode=OneWayToSource}" 
                  Grid.Row="0" Grid.Column="1" Margin="5" />
        <CheckBox IsChecked="{Binding FilterYears}" Content="Years" 
             Grid.Row="1"
             Grid.Column="0" Margin="5" />
        <Grid Grid.Row="1" Grid.Column="1" Margin="5">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <TextBlock Text="From" Grid.Row="0" Grid.Column="0" 
                       VerticalAlignment="Center" />
            <TextBlock Text="To" Grid.Row="1" Grid.Column="0" 
                       VerticalAlignment="Center" />
            <Slider x:Name="minSlider" Grid.Row="0" Grid.Column="1" 
                    IsSelectionRangeEnabled="True" 
                         IsSnapToTickEnabled="True" 
                    TickFrequency="5" TickPlacement="BottomRight" 
                    AutoToolTipPlacement="TopLeft" Margin="5" 
                    Minimum="{Binding MinYear, Mode=OneTime}" 
                    Maximum="{Binding MaxYear, Mode=OneTime}"
                    Value="{Binding SelectedMinYear}"
                    SelectionStart="{Binding MinYear, Mode=OneWay}"
                    SelectionEnd=
                        "{Binding ElementName=maxSlider, Path=Value, 
                          Mode=OneWay}" />
            <Slider x:Name="maxSlider" Grid.Row="1" Grid.Column="1" 
                    IsSelectionRangeEnabled="True"  
                         IsSnapToTickEnabled="True" 
                    TickFrequency="5" TickPlacement="BottomRight" 
                    AutoToolTipPlacement="TopLeft" Margin="5" 
                    Minimum="{Binding MinYear, Mode=OneTime}" 
                    Maximum="{Binding MaxYear, Mode=OneTime}" 
                    Value="{Binding SelectedMaxYear}"
                    SelectionStart=
                        "{Binding ElementName=minSlider, Path=Value, 
                        Mode=OneWay}" 
                    SelectionEnd="{Binding MaxYear, Mode=OneWay}" />
        </Grid>
        <Button Command="{Binding GetRacersCommand}" 
                Content="Get Racers" Grid.Row="2" Grid.ColumnSpan="2" 
                HorizontalAlignment="Center" Margin="5" Padding="3" />
     </Grid>
</GroupBox>

6.3 列表绑定步骤

  1. 在 ViewModel 中定义相关属性和命令。
  2. 在 XAML 中使用 CheckBox ComboBox Slider 等控件进行筛选设置。
  3. 绑定 ListBox ItemsSource 属性到 ViewModel Racers 属性。

总结

通过本文的介绍,我们了解了 WPF 中数据绑定的多种方式,包括命令绑定、简单数据绑定、值转换、多属性绑定和列表绑定。这些技术能够帮助我们实现数据与界面的分离,提高代码的可维护性和可测试性。在实际开发中,可以根据具体需求选择合适的数据绑定方式。

关键技术点总结

技术点 描述
命令绑定 通过 CommandBindings 将命令与事件处理程序关联
MVVM 和 DelegateCommand 在 MVVM 模式中实现命令和处理程序的松散耦合
简单数据绑定 TextBlock 等控件的属性绑定到数据属性
值转换 使用转换器处理不同类型的绑定
多属性绑定 使用 MultiBinding 将多个属性绑定到一个 UI 元素
列表绑定 使用 ItemsControl 绑定列表数据,并实现筛选功能

流程图:数据绑定流程

graph LR
    A[定义命令和事件处理程序] --> B[创建 ViewModel 类]
    B --> C[在 XAML 中绑定命令和数据]
    C --> D[执行命令,更新数据]
    D --> E[显示数据到界面]
    F[定义筛选条件] --> G[筛选数据]
    G --> E

通过以上步骤和示例代码,你可以在 WPF 应用程序中灵活运用数据绑定技术,实现高效、可维护的界面开发。

6.4 筛选功能的实现原理

ShowRacersViewModel 类中,筛选功能主要通过 GetExpression 方法实现。该方法根据 FilterCountry FilterYears 属性的值对赛车手数据进行筛选。
- 国家筛选 :如果 FilterCountry true ,则使用 Where 方法筛选出国籍与 SelectedCountry 相同的赛车手。
- 年份筛选 :如果 FilterYears true ,则使用 SelectMany 方法展开赛车手的比赛结果,然后筛选出比赛年份在 SelectedMinYear SelectedMaxYear 之间的赛车手,并使用 Distinct 方法去除重复项。

6.5 筛选功能的操作步骤

  1. 设置筛选条件 :在 UI 中,通过 CheckBox 控件选择是否启用国家和年份筛选。
  2. 选择筛选值 :如果启用国家筛选,通过 ComboBox 选择国家;如果启用年份筛选,通过 Slider 控件选择年份范围。
  3. 触发筛选 :点击“Get Racers”按钮,触发 GetRacersCommand 命令,调用 GetRacers 方法,通知 UI Racers 属性发生变化,从而更新列表显示。

7. 数据绑定的优化建议

7.1 性能优化

  • 减少不必要的绑定 :避免在界面中绑定过多不必要的数据,只绑定需要显示或交互的数据,减少数据更新时的性能开销。
  • 使用延迟加载 :对于一些大数据集,可以使用延迟加载的方式,只在需要时加载数据,避免一次性加载过多数据导致性能下降。
  • 优化查询语句 :在 ViewModel 中,优化数据查询语句,避免复杂的嵌套查询和不必要的计算,提高数据查询的效率。

7.2 代码可维护性优化

  • 封装业务逻辑 :将业务逻辑封装在 ViewModel 中,避免在 XAML 中编写过多的代码,提高代码的可维护性和可测试性。
  • 使用命名约定 :在命名 ViewModel 的属性和命令时,使用清晰、有意义的命名约定,方便团队成员理解和维护代码。
  • 模块化开发 :将不同的功能模块拆分成独立的 ViewModel 和视图,降低代码的耦合度,提高代码的可扩展性。

7.3 数据验证优化

  • 实现数据验证逻辑 :在 ViewModel 中实现数据验证逻辑,确保用户输入的数据符合要求。可以使用 IDataErrorInfo 接口或 INotifyDataErrorInfo 接口实现数据验证。
  • 显示验证错误信息 :在 UI 中显示数据验证错误信息,提示用户输入正确的数据。可以使用 Validation.ErrorTemplate 自定义验证错误信息的显示样式。

8. 常见问题及解决方案

8.1 绑定失败问题

  • 问题描述 :在 XAML 中绑定数据时,数据无法正确显示。
  • 解决方案
    • 检查绑定路径 :确保绑定路径正确,包括属性名和命名空间。
    • 检查 DataContext :确保 DataContext 已正确设置,并且包含绑定的属性。
    • 检查数据类型 :确保绑定的数据类型与目标属性的数据类型兼容。

8.2 命令无法执行问题

  • 问题描述 :点击按钮或执行命令时,命令没有被执行。
  • 解决方案
    • 检查命令绑定 :确保命令绑定正确,包括命令名和事件处理程序。
    • 检查 CanExecute 方法 :确保 CanExecute 方法返回 true ,允许命令执行。
    • 检查 CommandManager :确保 CommandManager 已正确初始化,并且能够正确处理命令。

8.3 数据更新不及时问题

  • 问题描述 :数据发生变化时,UI 没有及时更新。
  • 解决方案
    • 实现 INotifyPropertyChanged 接口 :在 ViewModel 中实现 INotifyPropertyChanged 接口,确保属性变化时能够通知 UI 更新。
    • 检查绑定模式 :确保绑定模式设置正确,如 OneWay TwoWay OneTime

9. 案例分析:完整的 WPF 数据绑定应用

9.1 应用场景

假设我们要开发一个赛车手管理系统,需要显示赛车手列表,并支持按国家和年份筛选。用户可以输入筛选条件,点击按钮获取筛选后的赛车手列表。

9.2 实现步骤

  1. 创建 ViewModel :创建 ShowRacersViewModel 类,定义赛车手数据、筛选条件和命令。
  2. 创建视图 :创建 ShowRacersView.xaml 文件,使用 CheckBox ComboBox Slider ListBox 等控件实现 UI,并绑定数据和命令。
  3. 实现筛选功能 :在 ViewModel 中实现 GetExpression 方法,根据筛选条件筛选赛车手数据。
  4. 处理命令 :在 ViewModel 中实现 GetRacersCommand 命令,点击按钮时调用 GetRacers 方法,通知 UI 更新赛车手列表。

9.3 代码示例

以下是完整的代码示例:

// ShowRacersViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Formula1.Infrastructure;
using Formula1.Model;

namespace Formula1.ViewModels
{
    public class ShowRacersViewModel : ViewModelBase, IDisposable
    {
        private Formula1Entities data;
        public ShowRacersViewModel()
        {
            if (!IsDesignTime)
            {
                data = new Formula1Entities();
            }            
        }
        private DelegateCommand getRacersCommand;
        public DelegateCommand GetRacersCommand
        {
            get
            {
                return getRacersCommand ??
                    (getRacersCommand = new DelegateCommand(
                        param => this.GetRacers()));
            }
        }

        public bool FilterCountry { get; set; }
        public bool FilterYears { get; set; }

        private string[] countries;
        public IEnumerable<string> Countries
        {
            get
            {
                return countries ?? 
                    (countries = data.Racers.Select(
                        r => r.Nationality).Distinct().ToArray());
            }
        }
        public string SelectedCountry { get; set; }

        private int minYear;
        public int MinYear
        {
            get
            {
                if (IsDesignTime)
                    minYear = 1950;
                return minYear != 0 ? minYear : minYear = 
                    data.Races.Select(r => r.Date.Year).Min();
            }
        }

        private int maxYear;
        public int MaxYear
        {
            get
            {
                if (IsDesignTime)
                    maxYear = DateTime.Today.Year;
                return maxYear != 0 ? maxYear : maxYear = 
                    data.Races.Select(r => r.Date.Year).Max();
            }
        }

        private int selectedMinYear;
        public int SelectedMinYear
        {
            get
            {
                return selectedMinYear;
            }
            set
            {
                if (!object.Equals(selectedMinYear, value))
                {
                    selectedMinYear = value;
                    RaisePropertyChanged("SelectedMinYear");
                }
            }
        }
        private int selectedMaxYear;
        public int SelectedMaxYear
        {
            get
            {
                return selectedMaxYear;
            }
            set
            {
                if (!object.Equals(selectedMaxYear, value))
                {
                    selectedMaxYear = value;
                    RaisePropertyChanged("SelectedMaxYear");
                }
            }
        }

        public void Dispose()
        {
            data.Dispose();
        }

        private void GetRacers()
        {
            RaisePropertyChanged("Racers");
        }

        private IQueryable<Racer> GetExpression()
        {
            var expr = data.Racers as IQueryable<Racer>;
            if (FilterCountry)
            {
                expr = expr.Where(r => r.Nationality == this.SelectedCountry);
            }
            if (FilterYears)
            {
                expr = expr.SelectMany(
                    r => r.RaceResults,
                    (r1, raceResult) => new { Racer = r1, RaceResult = 
                         raceResult })
                    .Where(raceInfo => 
                        raceInfo.RaceResult.Race.Date.Year >= SelectedMinYear &&
                        raceInfo.RaceResult.Race.Date.Year <= SelectedMaxYear)
                    .Select(raceInfo => raceInfo.Racer)
                    .Distinct();
            }                    
            return expr;                    
        }

        public IQueryable<Racer> Racers
        {
            get
            {
                return GetExpression();
            }
        }
    }
}
<!-- ShowRacersView.xaml -->
<GroupBox Header="Filter" Grid.Row="0">
    <Grid Grid.Row="0">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <CheckBox IsChecked="{Binding FilterCountry}" Content="Country"
                  Grid.Row="0" Grid.Column="0" Margin="5" />
        <ComboBox ItemsSource="{Binding Countries}" 
                  SelectedItem="{Binding SelectedCountry, 
                      Mode=OneWayToSource}" 
                  Grid.Row="0" Grid.Column="1" Margin="5" />
        <CheckBox IsChecked="{Binding FilterYears}" Content="Years" 
             Grid.Row="1"
             Grid.Column="0" Margin="5" />
        <Grid Grid.Row="1" Grid.Column="1" Margin="5">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <TextBlock Text="From" Grid.Row="0" Grid.Column="0" 
                       VerticalAlignment="Center" />
            <TextBlock Text="To" Grid.Row="1" Grid.Column="0" 
                       VerticalAlignment="Center" />
            <Slider x:Name="minSlider" Grid.Row="0" Grid.Column="1" 
                    IsSelectionRangeEnabled="True" 
                         IsSnapToTickEnabled="True" 
                    TickFrequency="5" TickPlacement="BottomRight" 
                    AutoToolTipPlacement="TopLeft" Margin="5" 
                    Minimum="{Binding MinYear, Mode=OneTime}" 
                    Maximum="{Binding MaxYear, Mode=OneTime}"
                    Value="{Binding SelectedMinYear}"
                    SelectionStart="{Binding MinYear, Mode=OneWay}"
                    SelectionEnd=
                        "{Binding ElementName=maxSlider, Path=Value, 
                          Mode=OneWay}" />
            <Slider x:Name="maxSlider" Grid.Row="1" Grid.Column="1" 
                    IsSelectionRangeEnabled="True"  
                         IsSnapToTickEnabled="True" 
                    TickFrequency="5" TickPlacement="BottomRight" 
                    AutoToolTipPlacement="TopLeft" Margin="5" 
                    Minimum="{Binding MinYear, Mode=OneTime}" 
                    Maximum="{Binding MaxYear, Mode=OneTime}" 
                    Value="{Binding SelectedMaxYear}"
                    SelectionStart=
                        "{Binding ElementName=minSlider, Path=Value, 
                        Mode=OneWay}" 
                    SelectionEnd="{Binding MaxYear, Mode=OneWay}" />
        </Grid>
        <Button Command="{Binding GetRacersCommand}" 
                Content="Get Racers" Grid.Row="2" Grid.ColumnSpan="2" 
                HorizontalAlignment="Center" Margin="5" Padding="3" />
     </Grid>
</GroupBox>
<ListBox ItemsSource="{Binding Racers}" Grid.Row="1" Grid.ColumnSpan="2" Margin="5" />

9.4 运行效果

运行该应用程序,用户可以在 UI 中设置筛选条件,点击“Get Racers”按钮,即可看到筛选后的赛车手列表。

总结

通过本文的介绍,我们全面了解了 WPF 数据绑定的各种技术,包括命令绑定、简单数据绑定、值转换、多属性绑定和列表绑定。同时,我们还学习了如何实现筛选功能、优化数据绑定性能、解决常见问题,并通过一个完整的案例展示了如何在实际应用中运用这些技术。

关键知识点回顾

知识点 要点
命令绑定 通过 CommandBindings 关联命令和事件处理程序,在 MVVM 中使用 DelegateCommand 实现松散耦合
简单数据绑定 将控件属性绑定到数据属性,设置 DataContext 实现数据显示
值转换 使用 IValueConverter 处理不同类型的绑定
多属性绑定 使用 MultiBinding IMultiValueConverter 将多个属性绑定到一个 UI 元素
列表绑定 使用 ItemsControl 绑定列表数据,实现筛选功能
筛选功能 ViewModel 中实现 GetExpression 方法进行数据筛选
优化建议 从性能、可维护性和数据验证方面优化数据绑定
常见问题解决 解决绑定失败、命令无法执行和数据更新不及时等问题

未来展望

WPF 数据绑定技术不断发展,未来可能会有更多的优化和扩展。例如,支持更复杂的数据类型绑定、提供更强大的筛选和排序功能等。开发者可以持续关注 WPF 技术的发展,不断提升自己的开发技能,为用户提供更优质的应用程序。

流程图:完整应用流程

graph LR
    A[启动应用] --> B[显示筛选界面]
    B --> C[设置筛选条件]
    C --> D[点击获取按钮]
    D --> E[执行命令,筛选数据]
    E --> F[更新列表显示]
    G[修改筛选条件] --> C

通过以上内容,希望你能在 WPF 开发中熟练运用数据绑定技术,打造出高效、可维护的应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值