C# – Dynamically adding elements to a UI in C#

cuser interfacewinformswpf

The Problem

I have a C# window with some text fields and buttons on it. It starts out similar to this:
The initial interface

When the user clicks that "+ Add Machine Function" button, I need to create a new row of controls and move the button below those:
Added machine function

If the user clicks "+Add Scale Unit" the program needs to add some controls to the right:
Added scale unit

Attempts at a solution

I have tried using Windows Forms' TableLayoutPanel but it seemed to handle resizing itself to fit additional controls in odd ways, for example it would some one rows of controls much wider than the others, and would make some rows so short it cut off parts of my controls.

I have also tried simply placing the controls by themselves into the form by simply calculating their relative positions. However I feel that this is bad programming practice as it makes the layout of the form relatively hard to change later. In the case of the user deleting the row or scale unit by pressing the 'X' beside it, this method also requires the program to find each element below that one and move it up individually which is terribly inefficient.

My question is: how would I go about creating a dynamically growing/shrinking application, either through Windows Forms layouts or WPF or something else?

Best Answer

In WPF you can do this:

Classes

public class MachineFunction
{
    public string Name { get; set; }
    public int Machines { get; set; }

    public ObservableCollection<ScaleUnit> ScaleUnits { get; set; }

    public MachineFunction()
    {
        ScaleUnits = new ObservableCollection<ScaleUnit>();
    }
}

public class ScaleUnit
{
    public string Name { get; set; }
    public int Index { get; set; }

    public ScaleUnit(int index)
    {
        this.Index = index;
    }
}

Window.xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
<StackPanel>
    <ItemsControl Name="lstMachineFunctions">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition/>
                            <ColumnDefinition/>
                            <ColumnDefinition/>
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition/>
                            <RowDefinition/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <TextBlock Grid.Row="0" Grid.Column="1" Text="Machine Function"/>
                        <TextBlock Grid.Row="0" Grid.Column="2" Text="Number of Machines"/>
                        <Button Grid.Row="1" Grid.Column="0" Click="OnDeleteMachineFunction">X</Button>
                        <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Name}"/>
                        <TextBox Grid.Row="1" Grid.Column="2" Text="{Binding Machines}"/>
                    </Grid>

                    <ItemsControl ItemsSource="{Binding ScaleUnits}">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <Grid Margin="12,0,0,0">
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition/>
                                        <ColumnDefinition/>
                                    </Grid.ColumnDefinitions>
                                    <Grid.RowDefinitions>
                                        <RowDefinition/>
                                        <RowDefinition/>
                                        <RowDefinition/>
                                    </Grid.RowDefinitions>
                                    <TextBlock Grid.Row="0" Grid.Column="1" Text="Machine/Scale Unit"/>
                                    <Button Grid.Row="1" Grid.Column="0" Click="OnDeleteScaleUnit">X</Button>
                                    <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Name}"/>
                                    <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Index, StringFormat='Scale Unit {0}'}"/>
                                </Grid>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <StackPanel Orientation="Horizontal"/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                    </ItemsControl>
                    <Button VerticalAlignment="Center" Click="OnAddScaleUnit">Add Scale Unit</Button>
                </StackPanel>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

    <Button HorizontalAlignment="Left" Click="OnAddMachineFunction">Add Machine Function</Button>
</StackPanel>
</Window>

Window.cs

public partial class MainWindow : Window
{
    public ObservableCollection<MachineFunction> MachineFunctions { get; set; }

    public MainWindow()
    {
        InitializeComponent();

        lstMachineFunctions.ItemsSource = MachineFunctions = new ObservableCollection<MachineFunction>();
    }

    private void OnDeleteMachineFunction(object sender, RoutedEventArgs e)
    {
        MachineFunctions.Remove((sender as FrameworkElement).DataContext as MachineFunction);
    }

    private void OnAddMachineFunction(object sender, RoutedEventArgs e)
    {
        MachineFunctions.Add(new MachineFunction());   
    }

    private void OnAddScaleUnit(object sender, RoutedEventArgs e)
    {
        var mf = (sender as FrameworkElement).DataContext as MachineFunction;

        mf.ScaleUnits.Add(new ScaleUnit(mf.ScaleUnits.Count));
    }

    private void OnDeleteScaleUnit(object sender, RoutedEventArgs e)
    {
        var delScaleUnit = (sender as FrameworkElement).DataContext as ScaleUnit;

        var mf = MachineFunctions.FirstOrDefault(_ => _.ScaleUnits.Contains(delScaleUnit));

        if( mf != null )
        {
            mf.ScaleUnits.Remove(delScaleUnit);

            foreach (var scaleUnit in mf.ScaleUnits)
            {
                scaleUnit.Index = mf.ScaleUnits.IndexOf(scaleUnit);
            }
        }
    }
}