如何基于MVVM在WPF中实现用户运行时选字段的动态数据录入表单?
WPF动态表单的MVVM重构实现方案
问题背景
我开发了一款WPF应用,支持用户在运行时选择字段列表并存储到文本文件,需要基于这些字段生成数据录入表单。目前已通过代码后置实现该功能,希望采用MVVM模式重构。我考虑了两种方案,现在需要具体的MVVM实现方法。
原方案分析
方案1:代码后置动态创建控件
通过代码后置读取文本文件,动态生成TextBlock和TextBox并添加到容器中。这种方式直接但违背MVVM的分离原则,视图逻辑和业务逻辑耦合严重,难以维护和测试。
原XAML代码:
<TabControl Margin="335,10,10,71" TabStripPlacement="Bottom"> <TabControl.Resources> <Style TargetType="{x:Type TabItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type TabItem}"> <Grid> <Border Name="Border" Margin="0,0,0,0" Background="Transparent" BorderBrush="Black" BorderThickness="1,1,1,1" CornerRadius="5"> <ContentPresenter x:Name="ContentSite" Margin="12,2,12,2" HorizontalAlignment="Center" VerticalAlignment="Center" ContentSource="Header" RecognizesAccessKey="True"> <ContentPresenter.LayoutTransform> <RotateTransform Angle="0" /> </ContentPresenter.LayoutTransform> </ContentPresenter> </Border> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="Height" Value="40" /> <Setter Property="FontSize" Value="20" /> <Setter Property="FontWeight" Value="Bold" /> <Setter Property="Panel.ZIndex" Value="200" /> <Setter TargetName="Border" Property="Background" Value="Black" /> <Setter TargetName="Border" Property="BorderBrush" Value="White" /> <Setter TargetName="Border" Property="BorderThickness" Value="1,1,1,0" /> <Setter Property="Foreground" Value="White" /> </Trigger> <Trigger Property="IsEnabled" Value="False"> <Setter Property="FontSize" Value="20" /> <Setter Property="FontWeight" Value="Bold" /> <Setter TargetName="Border" Property="Background" Value="Black" /> <Setter TargetName="Border" Property="BorderBrush" Value="White" /> <Setter Property="Foreground" Value="White" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </TabControl.Resources> <TabItem Header="Product Data 1"> <Grid x:Name="Grid1"> <Grid.ColumnDefinitions> <ColumnDefinition x:Name="Column_1" Width="*" /> <ColumnDefinition x:Name="Column_2" Width="*" /> </Grid.ColumnDefinitions> <Border Margin="0,0,0,10" Background="LightSkyBlue" BorderBrush="Gray" BorderThickness="2,2,2,2"> <StackPanel x:Name="MainStack" Margin="20,0,0,0" x:FieldModifier="public" Grid.IsSharedSizeScope="True" Orientation="Vertical" /> </Border> <Border Grid.Column="1" Margin="0,0,0,10" Background="LightSkyBlue" BorderBrush="Gray" BorderThickness="2,2,2,2"> <StackPanel x:Name="SecondStack" Grid.Column="1" Margin="20,0,0,0" x:FieldModifier="public" Grid.IsSharedSizeScope="True" Orientation="Vertical" /> </Border> </Grid> </TabItem> </TabControl>
原代码后置:
string[] Data = File.ReadAllLines("Field.txt"); foreach (var part in Data) { Grid mytxtBStack = new Grid(); MainStack.Children.Add(mytxtBStack); ColumnDefinition column_1 = new ColumnDefinition(); column_1.SharedSizeGroup = "FirstColumn"; ColumnDefinition column_2 = new ColumnDefinition(); column_2.SharedSizeGroup = "SecondColumn"; mytxtBStack.ColumnDefinitions.Add(column_1); mytxtBStack.ColumnDefinitions.Add(column_2); txtBlock = new TextBlock(); txtBlock.Name = "MyBlock" + i; txtBlock.Text = part; txtBlock.FontSize = 14; txtBlock.FontWeight = FontWeights.DemiBold; txtBlock.Foreground = Brushes.Black; txtBlock.Margin = new Thickness(0, 20, 0, 0); mytxtBStack.Children.Add(txtBlock); txtBox = new TextBox(); txtBox.Name = "MyText" + i.ToString(); txtBox.FontSize = 12; txtBox.Height = 25; txtBox.Width = 250; txtBox.BorderThickness = new Thickness(1, 1, 1, 1); txtBox.Margin = new Thickness(130, 20, 0, 0); txtBox.Foreground = Brushes.Black; txtBox.BorderBrush = Brushes.Black; txtBox.Background = Brushes.White; txtList.Add(txtBox); mytxtBStack.Children.Add(txtBox); }
方案2:ExpandoObject动态类+ItemsControl
思路是用ExpandoObject创建动态属性,通过ItemsControl绑定字段列表。但原代码的绑定存在问题(TextBox的Binding没有指向动态属性),需要完善。
原拟添加的XAML代码:
<ItemsControl Grid.Row="1" ItemsSource="{Binding Path=Fields}"> <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding}" /> <TextBox Width="300" Text="{Binding}" /> </StackPanel> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
具体MVVM实现方案
采用动态字段模型+ViewModel驱动+ItemsControl数据模板的方式,完全遵循MVVM分离原则,实现视图与逻辑解耦。
1. 动态字段模型类
创建DynamicField类,封装字段名称和用户输入值,实现INotifyPropertyChanged支持数据绑定:
public class DynamicField : INotifyPropertyChanged { private string _fieldName; private string _fieldValue; public string FieldName { get => _fieldName; set { _fieldName = value; OnPropertyChanged(nameof(FieldName)); } } public string FieldValue { get => _fieldValue; set { _fieldValue = value; OnPropertyChanged(nameof(FieldValue)); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
2. 主ViewModel类
ViewModel负责读取字段文件、管理动态字段列表,并提供数据保存等业务逻辑:
public class MainViewModel : INotifyPropertyChanged { private ObservableCollection<DynamicField> _dynamicFields; public ObservableCollection<DynamicField> DynamicFields { get => _dynamicFields; set { _dynamicFields = value; OnPropertyChanged(nameof(DynamicFields)); } } public MainViewModel() { LoadFieldsFromFile(); } private void LoadFieldsFromFile() { DynamicFields = new ObservableCollection<DynamicField>(); if (File.Exists("Field.txt")) { string[] fieldNames = File.ReadAllLines("Field.txt"); foreach (var name in fieldNames) { DynamicFields.Add(new DynamicField { FieldName = name }); } } } // 示例:保存所有字段值到文件 public void SaveFieldValues() { var lines = DynamicFields.Select(f => $"{f.FieldName}:{f.FieldValue}"); File.WriteAllLines("FieldValues.txt", lines); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
3. 视图改造(XAML)
移除代码后置的控件创建逻辑,改用ItemsControl结合数据模板渲染动态表单,保留原有的TabControl样式:
<TabControl Margin="335,10,10,71" TabStripPlacement="Bottom"> <TabControl.Resources> <!-- 保留原有的TabItem样式 --> <Style TargetType="{x:Type TabItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type TabItem}"> <Grid> <Border Name="Border" Margin="0,0,0,0" Background="Transparent" BorderBrush="Black" BorderThickness="1,1,1,1" CornerRadius="5"> <ContentPresenter x:Name="ContentSite" Margin="12,2,12,2" HorizontalAlignment="Center" VerticalAlignment="Center" ContentSource="Header" RecognizesAccessKey="True"> <ContentPresenter.LayoutTransform> <RotateTransform Angle="0" /> </ContentPresenter.LayoutTransform> </ContentPresenter> </Border> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="Height" Value="40" /> <Setter Property="FontSize" Value="20" /> <Setter Property="FontWeight" Value="Bold" /> <Setter Property="Panel.ZIndex" Value="200" /> <Setter TargetName="Border" Property="Background" Value="Black" /> <Setter TargetName="Border" Property="BorderBrush" Value="White" /> <Setter TargetName="Border" Property="BorderThickness" Value="1,1,1,0" /> <Setter Property="Foreground" Value="White" /> </Trigger> <Trigger Property="IsEnabled" Value="False"> <Setter Property="FontSize" Value="20" /> <Setter Property="FontWeight" Value="Bold" /> <Setter TargetName="Border" Property="Background" Value="Black" /> <Setter TargetName="Border" Property="BorderBrush" Value="White" /> <Setter Property="Foreground" Value="White" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <!-- 动态字段项的数据模板 --> <DataTemplate x:Key="DynamicFieldTemplate"> <Grid Margin="0,20,0,0"> <Grid.ColumnDefinitions> <ColumnDefinition SharedSizeGroup="FirstColumn"/> <ColumnDefinition SharedSizeGroup="SecondColumn"/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="{Binding FieldName}" FontSize="14" FontWeight="DemiBold" Foreground="Black"/> <TextBox Grid.Column="1" Text="{Binding FieldValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" FontSize="12" Height="25" Width="250" BorderThickness="1" Margin="130,0,0,0" Foreground="Black" BorderBrush="Black" Background="White"/> </Grid> </DataTemplate> </TabControl.Resources> <TabItem Header="Product Data 1"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Border Margin="0,0,0,10" Background="LightSkyBlue" BorderBrush="Gray" BorderThickness="2"> <!-- 第一列表单 --> <ItemsControl Margin="20,0,0,0" ItemsSource="{Binding DynamicFields}" ItemTemplate="{StaticResource DynamicFieldTemplate}" Grid.IsSharedSizeScope="True"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Vertical"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> </Border> <!-- 如果需要第二列分栏,可以拆分DynamicFields为两个集合绑定 --> <Border Grid.Column="1" Margin="0,0,0,10" Background="LightSkyBlue" BorderBrush="Gray" BorderThickness="2"> <!-- 示例:绑定后半部分字段 --> <ItemsControl Margin="20,0,0,0" ItemsSource="{Binding DynamicFields}" ItemTemplate="{StaticResource DynamicFieldTemplate}" Grid.IsSharedSizeScope="True"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Vertical"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemContainerStyle> <Style> <Setter Property="Visibility" Value="Collapsed"/> <Style.Triggers> <DataTrigger Binding="{Binding RelativeSource={RelativeSource ItemIndex}}" Value="0"> <Setter Property="Visibility" Value="Visible"/> </DataTrigger> <!-- 根据需要调整显示的字段索引 --> </Style.Triggers> </Style> </ItemsControl.ItemContainerStyle> </ItemsControl> </Border> </Grid> </TabItem> </TabControl>
4. 视图绑定ViewModel
在窗口构造函数中设置DataContext:
public MainWindow() { InitializeComponent(); DataContext = new MainViewModel(); }
或者在XAML中直接声明:
<Window.DataContext> <local:MainViewModel/> </Window.DataContext>
方案优势
- 完全遵循MVVM原则,视图仅负责渲染,业务逻辑集中在ViewModel
- 动态字段通过ObservableCollection管理,支持自动更新视图
- 数据绑定采用TwoWay模式,用户输入实时同步到ViewModel
- 可扩展性强,新增字段类型(如下拉框、日期选择器)只需修改DataTemplate
内容的提问来源于stack exchange,提问作者R J
相关产品推荐
相关产品推荐

