Dynamically adding elements to a UI in C#

17,895

Solution 1

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

Solution 2

I did the same thing recently in WinForms and the way I did it was as follows:

Create a UserControl that contains the controls I wanted to repeat

enter image description here

Add a FlowLayoutPanel to the main form to contain all the user controls (and to simplify their positioning)

enter image description here

Add a new instance of your custom UserControl to the FlowLayoutPanel every time you want a new "row" of controls

flowLayoutPanel1.Controls.Add(
            new MachineFunctionUC {
                Parent = flowLayoutPanel1
            });

To remove a row of control call this.Dispose(); from within the user control (that's the instruction executed by the "X" button).

enter image description here

If you want the UserControls to be arranged vertically set the following properties:

flowLayoutPanel1.AutoScroll = true;
flowLayoutPanel1.WrapContents = false;
flowLayoutPanel1.FlowDirection = System.Windows.Forms.FlowDirection.TopDown;

And to access them use flowLayoutPanel1.Controls[..]

Solution 3

The correct way to achieve your requirements in WPF is for you to define a custom data type class to represent your machine function. Provide it with how ever many properties that you need to represent your machine fields. When you have done this, you then need to move the code that generated your machine function UI into a DataTemplate for the type of your class and data bind all of the relevant properties:

<DataTemplate DataType="{Binding YourPrefix:MachineFunction}">
    ...
</DataTemplate>

Then, you need to create a collection property to hold your machine function items and data bind that to some kind of collection control. Once you have done this, then to add another row, you just need to add another item to the collection:

<ItemsControl ItemsSource="{Binding MachineFunctions}">
    <ItemsControl.Resources>
        <DataTemplate DataType="{Binding YourPrefix:MachineFunction}">
            ...
        </DataTemplate>
    </ItemsControl.Resources>
</ItemsControl>
<Button Content="+ Add Machine Function" ... />

...

MachineFunctions.Add(new MachineFunction());

Please see the Data Binding Overview page on MSDN for further help with data binding.

Solution 4

Create a function which will define a row for you. Consider the code and use its where to place another control and do as for buttons also and count it position.

Button button1=new Button();
button1.Text="dynamic button";
button1.Left=10; button1.Top=10;  //the button's location
this.Controls.Add(button1); //this is how you can add control
Share:
17,895
AtomicStrongForce
Author by

AtomicStrongForce

Updated on June 28, 2022

Comments

  • AtomicStrongForce
    AtomicStrongForce almost 2 years

    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?