运行时用GridView绘制链表:如何实现数据绑定?
手动往Grid里硬塞控件确实不够优雅,WPF的数据绑定正好能解决这个问题——让UI自动跟着数据源的变化同步更新,不用再手动维护控件的增删。下面是具体的实现步骤:
1. 先让数据源支持UI通知
要实现自动更新,你的数据源必须能在元素增减时通知UI。最省心的方式是用WPF自带的ObservableCollection<T>,它已经内置了INotifyCollectionChanged接口,集合一有变化就会自动触发UI更新。
如果一定要保留你自定义的ILinkedList,那得给它补上INotifyCollectionChanged接口的实现(后面会讲),但建议先用ObservableCollection快速验证效果:
// 放在你的ViewModel类里 public ObservableCollection<string> LinkedItems { get; } = new ObservableCollection<string>();
2. 用ItemsControl替代手动创建控件
ItemsControl是WPF专门用来展示集合的控件,完全可以替代你之前手动创建TextBlock和Path的逻辑。修改你的XAML,把原来的多列Grid换成ItemsControl:
<Grid x:Name="ListDrawGrid"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="2*" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <!-- 用ItemsControl展示链表元素,放在第二行 --> <ItemsControl x:Name="LinkedListItemsControl" Grid.Row="1" ItemsSource="{Binding LinkedItems}" AlternationCount="100"> <!-- 给元素索引判断用 --> <!-- 设置元素横向排列的面板,模拟你原来的多列布局 --> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <UniformGrid Columns="6" <!-- 和你原来的MaxCols一致 --> VerticalAlignment="Center"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <!-- 定义单个链表元素的UI模板:文本+红色分隔线 --> <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal" Margin="0,0,5,0"> <TextBlock Text="{Binding}" FontSize="15" FontWeight="Bold" Width="70" HorizontalAlignment="Center"/> <!-- 红色分隔线:最后一个元素要隐藏,后面处理 --> <Path Stroke="Red" StrokeThickness="4" Width="25" HorizontalAlignment="Center" Visibility="{Binding RelativeSource={RelativeSource AncestorType=ContentPresenter}, Converter={local:HideLastLineConverter}}"> <Path.Data> <LineGeometry StartPoint="0,10" EndPoint="25,10"/> </Path.Data> </Path> </StackPanel> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <!-- 最后固定显示NULL节点 --> <TextBlock Grid.Row="1" Text="NULL" FontSize="15" VerticalAlignment="Center" Margin="10,0,0,0"/> </Grid>
3. 设置数据上下文让绑定生效
要让ItemsControl能找到你的LinkedItems集合,得给窗口/控件设置DataContext。比如在窗口构造函数里:
public MainWindow() { InitializeComponent(); // 假设你的ViewModel类叫LinkedListViewModel DataContext = new LinkedListViewModel(); }
或者直接在XAML里声明ViewModel:
<Window.DataContext> <local:LinkedListViewModel/> </Window.DataContext>
4. 改造Add/Delete逻辑,只更新数据源就行
原来的DrawList方法可以直接删掉了!现在你只需要更新LinkedItems集合,UI会自动同步。比如给Add按钮绑定命令(建议用MVVM的命令模式,避免代码后置):
首先在ViewModel里实现命令(这里用自定义的RelayCommand,你也可以用Prism等框架的现成实现):
public ICommand AddItemCommand { get; } public LinkedListViewModel() { AddItemCommand = new RelayCommand<string>(AddItem); } private void AddItem(string itemText) { if (!string.IsNullOrWhiteSpace(itemText)) { LinkedItems.Add(itemText); // 如果需要同步你的自定义ILinkedList,在这里调用它的Add方法就行 // _customLinkedList.Add(itemText); } } // 简单的RelayCommand实现 public class RelayCommand<T> : ICommand { private readonly Action<T> _execute; private readonly Func<T, bool> _canExecute; public event EventHandler CanExecuteChanged; public RelayCommand(Action<T> execute, Func<T, bool> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } public bool CanExecute(object parameter) => _canExecute?.Invoke((T)parameter) ?? true; public void Execute(object parameter) => _execute((T)parameter); public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); }
然后在XAML里给按钮绑定命令和输入框的文本:
<StackPanel Orientation="Horizontal" Margin="10"> <TextBox x:Name="ItemInputBox" Width="150"/> <Button Content="Add Item" Command="{Binding AddItemCommand}" CommandParameter="{Binding Text, ElementName=ItemInputBox}" Margin="5,0"/> </StackPanel>
5. 处理最后一个元素的多余分隔线
上面的模板里每个元素都带分隔线,最后一个元素的线会显得多余。我们写个转换器来判断是否是最后一个元素,隐藏它的分隔线:
public class HideLastLineConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is ContentPresenter presenter && presenter.TemplatedParent is ItemsControl itemsControl) { var itemIndex = itemsControl.ItemContainerGenerator.IndexFromContainer(presenter); return itemIndex == itemsControl.Items.Count - 1 ? Visibility.Collapsed : Visibility.Visible; } return Visibility.Visible; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
别忘了在XAML里声明这个转换器:
<Window.Resources> <local:HideLastLineConverter x:Key="HideLastLineConverter"/> </Window.Resources>
6. 适配自定义ILinkedList(可选)
如果一定要保留你自己的ILinkedList,那得给它实现INotifyCollectionChanged接口,让它能通知UI变化:
public class CustomLinkedList<T> : ILinkedList<T>, INotifyCollectionChanged { // 你的链表核心逻辑... public event NotifyCollectionChangedEventHandler CollectionChanged; private void OnCollectionChanged(NotifyCollectionChangedAction action, T item = default) { CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(action, item)); } // 重写Add方法,添加完元素触发通知 public void Add(T item) { // 你的添加逻辑... OnCollectionChanged(NotifyCollectionChangedAction.Add, item); } // 重写Delete方法,删除完元素触发通知 public void Delete(T item) { // 你的删除逻辑... OnCollectionChanged(NotifyCollectionChangedAction.Remove, item); } }
之后直接把ItemsControl的ItemsSource绑定到这个自定义链表就行,UI会自动响应它的变化。
这样改造后,你点击Add按钮时只需要更新数据源,UI就会自动渲染新元素,再也不用手动创建和管理控件了,代码更简洁也更符合WPF的设计思想。
内容的提问来源于stack exchange,提问作者vinit

