通过咖啡店点单系统,了解WPF的特性

44 阅读4分钟

让我用一个生活中的例子——咖啡店点单系统,来解释一些WPF概念。

场景:咖啡店点单系统

1. WPF框架 - 整个咖啡店

csharp

// MainWindow.xaml.cs - 整个咖啡店店面
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        // 开店营业
        DataContext = new CoffeeShopViewModel();
    }
}

2. 数据绑定 - 点单与制作联动

就像顾客点单后,后厨自动收到订单开始制作:

csharp

// CoffeeOrder.cs - 订单模型
public class CoffeeOrder : INotifyPropertyChanged
{
    private string _coffeeType;
    public string CoffeeType
    {
        get => _coffeeType;
        set
        {
            _coffeeType = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(TotalPrice)); // 价格自动更新
        }
    }
    
    private int _size;
    public int Size
    {
        get => _size;
        set
        {
            _size = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(TotalPrice));
        }
    }
    
    // 自动计算总价(绑定到UI)
    public decimal TotalPrice => (CoffeeType == "拿铁" ? 25m : 20m) * Size;
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

3. 依赖属性 - 可定制的咖啡杯

就像咖啡杯可以有各种可自定义的特性(颜色、容量、材质):

csharp

// CoffeeCupControl.xaml.cs - 自定义咖啡杯控件
public class CoffeeCupControl : Control
{
    // 依赖属性:杯子容量(毫升)
    public static readonly DependencyProperty CapacityProperty =
        DependencyProperty.Register("Capacity", typeof(int), typeof(CoffeeCupControl),
            new FrameworkPropertyMetadata(300,
                FrameworkPropertyMetadataOptions.AffectsRender,
                OnCapacityChanged));
    
    public int Capacity
    {
        get => (int)GetValue(CapacityProperty);
        set => SetValue(CapacityProperty, value);
    }
    
    // 依赖属性:咖啡类型
    public static readonly DependencyProperty CoffeeTypeProperty =
        DependencyProperty.Register("CoffeeType", typeof(string), typeof(CoffeeCupControl),
            new FrameworkPropertyMetadata("美式", OnCoffeeTypeChanged));
    
    public string CoffeeType
    {
        get => (string)GetValue(CoffeeTypeProperty);
        set => SetValue(CoffeeTypeProperty, value);
    }
    
    private static void OnCapacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        // 当容量改变时,自动调整杯子的显示
        var cup = (CoffeeCupControl)d;
        cup.InvalidateVisual(); // 触发重绘
    }
}

4. 模板和样式 - 咖啡的呈现方式

就像同样的咖啡可以用不同杯子、不同装饰来呈现:

xml

<!-- CoffeeOrderTemplate.xaml - 订单显示模板 -->
<DataTemplate DataType="{x:Type local:CoffeeOrder}">
    <Border BorderBrush="Brown" BorderThickness="2" CornerRadius="8" Margin="5">
        <StackPanel Orientation="Horizontal" Padding="10">
            <!-- 根据咖啡类型显示不同图标 -->
            <ContentControl Content="{Binding CoffeeType}">
                <ContentControl.Style>
                    <Style TargetType="ContentControl">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding CoffeeType}" Value="拿铁">
                                <Setter Property="ContentTemplate">
                                    <Setter.Value>
                                        <DataTemplate>
                                            <Image Source="/Images/latte.png" Width="32" Height="32"/>
                                        </DataTemplate>
                                    </Setter.Value>
                                </Setter>
                            </DataTrigger>
                            <DataTrigger Binding="{Binding CoffeeType}" Value="美式">
                                <Setter Property="ContentTemplate">
                                    <Setter.Value>
                                        <DataTemplate>
                                            <Image Source="/Images/americano.png" Width="32" Height="32"/>
                                        </DataTemplate>
                                    </Setter.Value>
                                </Setter>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ContentControl.Style>
            </ContentControl>
            
            <StackPanel Margin="10,0">
                <TextBlock Text="{Binding CoffeeType}" FontWeight="Bold"/>
                <TextBlock Text="{Binding Size, StringFormat='{}{0}杯'}"/>
                <TextBlock Text="{Binding TotalPrice, StringFormat='¥{0}'}" 
                           Foreground="Red" FontWeight="Bold"/>
            </StackPanel>
        </StackPanel>
    </Border>
</DataTemplate>

5. ViewModel和命令 - 咖啡师的操作逻辑

csharp

// CoffeeShopViewModel.cs - 咖啡店的运营逻辑
public class CoffeeShopViewModel : INotifyPropertyChanged
{
    private ObservableCollection<CoffeeOrder> _orders;
    public ObservableCollection<CoffeeOrder> Orders
    {
        get => _orders;
        set
        {
            _orders = value;
            OnPropertyChanged();
        }
    }
    
    private CoffeeOrder _currentOrder;
    public CoffeeOrder CurrentOrder
    {
        get => _currentOrder;
        set
        {
            _currentOrder = value;
            OnPropertyChanged();
            // 命令的可执行状态会自动更新
            PlaceOrderCommand.RaiseCanExecuteChanged();
        }
    }
    
    // 命令:下单(就像咖啡师接收订单的动作)
    public RelayCommand PlaceOrderCommand { get; }
    public RelayCommand ClearOrdersCommand { get; }
    
    public CoffeeShopViewModel()
    {
        Orders = new ObservableCollection<CoffeeOrder>();
        CurrentOrder = new CoffeeOrder();
        
        PlaceOrderCommand = new RelayCommand(
            execute: () =>
            {
                Orders.Add(CurrentOrder);
                CurrentOrder = new CoffeeOrder(); // 清空当前订单,准备下一个
            },
            canExecute: () => CurrentOrder != null && 
                            !string.IsNullOrEmpty(CurrentOrder.CoffeeType) && 
                            CurrentOrder.Size > 0);
        
        ClearOrdersCommand = new RelayCommand(
            execute: () => Orders.Clear(),
            canExecute: () => Orders.Any());
    }
}

6. 性能优化 - 高效运营咖啡店

csharp

// 性能优化技巧示例
public class OptimizedCoffeeShop
{
    // 技巧1:虚拟化 - 只显示视窗内的订单(像只准备当前顾客能看到的咖啡)
    public void ApplyVirtualization()
    {
        // 在XAML中:VirtualizingStackPanel.IsVirtualizing="True"
        // 只创建和渲染可见的UI元素
    }
    
    // 技巧2:异步加载 - 非阻塞式准备咖啡
    public async Task PrepareCoffeeAsync(CoffeeOrder order)
    {
        // UI线程不被阻塞,顾客可以继续点单
        await Task.Run(() =>
        {
            // 模拟制作咖啡的耗时操作
            Thread.Sleep(2000);
        });
        
        // 完成后更新UI
        Application.Current.Dispatcher.Invoke(() =>
        {
            // 更新完成状态
        });
    }
    
    // 技巧3:延迟加载 - 按需加载配料
    public class LazyIngredient
    {
        private Lazy<List<string>> _ingredients = new Lazy<List<string>>(() =>
        {
            // 只有当真正需要时,才从数据库加载配料列表
            return LoadIngredientsFromDatabase();
        });
        
        public List<string> Ingredients => _ingredients.Value;
    }
    
    // 技巧4:数据虚拟化 - 处理大量历史订单
    public class VirtualizedOrderList : ICollectionView
    {
        // 只加载需要显示的数据,而不是全部加载到内存
    }
}

7. 使用示例:主界面

xml

<!-- MainWindow.xaml - 咖啡店点单界面 -->
<Window x:Class="CoffeeShop.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="星巴克WPF版" Height="600" Width="800">
    
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="300"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        
        <!-- 左边:点单区 -->
        <Border Grid.Column="0" Background="#FFF5F5F5" Padding="20">
            <StackPanel>
                <TextBlock Text="点单" FontSize="24" Margin="0,0,0,20"/>
                
                <ComboBox ItemsSource="{Binding CoffeeTypes}" 
                          SelectedItem="{Binding CurrentOrder.CoffeeType}"
                          Margin="0,0,0,10"/>
                
                <Slider Minimum="1" Maximum="10" 
                        Value="{Binding CurrentOrder.Size}"
                        Margin="0,0,0,10"/>
                
                <TextBlock Text="{Binding CurrentOrder.TotalPrice, StringFormat='总价: ¥{0}'}"
                           FontSize="16" Foreground="Red" Margin="0,0,0,20"/>
                
                <Button Content="下单" 
                        Command="{Binding PlaceOrderCommand}"
                        IsEnabled="{Binding PlaceOrderCommand.CanExecute}"
                        Background="Green" Foreground="White"
                        Padding="20,10"/>
            </StackPanel>
        </Border>
        
        <!-- 右边:订单列表(使用虚拟化提高性能) -->
        <Border Grid.Column="1" Padding="20">
            <ScrollViewer>
                <ItemsControl ItemsSource="{Binding Orders}"
                              VirtualizingStackPanel.IsVirtualizing="True"
                              VirtualizingStackPanel.VirtualizationMode="Recycling">
                    <ItemsControl.ItemTemplate>
                        <!-- 使用前面定义的模板 -->
                        <DataTemplate DataType="{x:Type local:CoffeeOrder}">
                            <!-- ... 模板内容 ... -->
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </ScrollViewer>
        </Border>
    </Grid>
</Window>

概念对应关系总结:

WPF概念咖啡店比喻作用
WPF框架整个咖啡店提供制作和展示的基础设施
数据绑定点单-制作联动自动同步前后台数据
依赖属性咖啡杯特性可继承、可动画、支持绑定的属性
模板咖啡呈现方式同一种数据的不同显示方式
样式统一装修风格保持UI一致性
命令咖啡师操作规范分离UI和业务逻辑
性能优化高效运营策略提升响应速度和用户体验

这个例子展示了WPF如何像一家高效的咖啡店一样工作:顾客(用户)通过界面点单(UI),订单(数据)自动传递给后厨(业务逻辑),咖啡师(ViewModel)按照规范(命令)制作,用合适的杯子(模板)呈现,整个过程高效流畅(性能优化)。