Search code examples
c#wpfmvvmwpf-controlsmvvmcross

Creating dynamic data entry form from the list of fields chosen by User at run time


I have a WPF application where I have view which lets User choose list of fields at run-time and I am storing in a Text file and I am trying to create a Data entry form based on the list of fields that user created Run-time.

I have developed an solution using code behind but I am trying to implement this using MVVM.

Approach 1: I can create the textBlock and Textbox in the code-behind and bind it to the properties in the Viewmodel. The viewmodel will have all the possible field property.

<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>
            <!--<StackPanel x:Name="ThirdStack" x:FieldModifier="public"  Grid.Column="2" Orientation="Vertical" Grid.IsSharedSizeScope="True" Width="auto" Height="476"  VerticalAlignment="Center" HorizontalAlignment="Center" />
            <Border BorderBrush="Black" BorderThickness="1,1,1,1" Grid.Column="2" Margin="0,0,0,10"/>-->
        </Grid>
    </TabItem>
</TabControl>

Code-Behind=

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);
}

Approach 2: I can create a dynamic class using the Expandoobject based on the Textfile and create a itemscontrols to bind with view and mentioned all the possibile field properties in the Viewmodel. I am thinking about implementing this part with inside the TabControl,

<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>

Can anyone please suggest me a way to do this with MVVM wpf?


Solution

  • I implemented this using the CommunityToolkit.Mvvm NuGet package.

    MainWindowViewModel.cs

    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;
    using System.Collections.ObjectModel;
    
    namespace WpfApp1;
    
    public class Field
    {
        public string Block { get; set; } = string.Empty;
        public string Text { get; set; } = string.Empty;
    }
    
    public partial class MainWindowViewModel : ObservableObject
    {
        [ObservableProperty]
        // CommunityToolkit's source generator will create a "Field" property.
        private ObservableCollection<Field> fields = new();
    
        [RelayCommand]
        // CommunityToolkit's source generator will create a "LoadFieldsCommand" command.
        private void LoadFields()
        {
            for (int i = 0; i < 10; i++)
            {
                Fields.Add(new Field() { Block = $"Block#{i + 1}", Text = $"Text{i + 1}" });
            }
        }
    
        [ObservableProperty]
        // Bind this to the Text property.
        private string textBlockText = string.Empty;
    
        [ObservableProperty]
        // Bind this to the ItemsSource property.
        private List<string> comboBoxItems = new()
        {
            "Item A",
            "Item B",
            "Item C",
        };
    }
    

    MainWindow.xaml.cs

    using System.Windows;
    
    namespace WpfApp1;
    
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    
        public MainWindowViewModel ViewModel { get; } = new();
    }
    

    MainWindow.xaml

    <Window
        x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:WpfApp1"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        x:Name="ThisWindow"
        Title="MainWindow"
        Width="800"
        Height="450"
        mc:Ignorable="d">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
    
            <Button
                Grid.Row="0"
                Command="{Binding ElementName=ThisWindow, Path=ViewModel.LoadFieldsCommand}"
                Content="Load fields" />
    
            <ItemsControl Grid.Row="1" ItemsSource="{Binding ElementName=ThisWindow, Path=ViewModel.Fields}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate DataType="local:Field">
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Block}" />
                            <TextBox Text="{Binding Text}" />
                        </StackPanel>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Grid>
    </Window>