You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 23:55:25