8

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?

3
  • 4
    In both WPF and Winform the easiest solution is to use a DataRepeater like UI component and bind it to a model object which has collections for each type of UI element you want on the screen. WPF works much better than WinForms for this. Commented Jul 14, 2014 at 2:22
  • 1
    In the TableLayoutPanel solution, you could set each row size to be (fixed size) the same or adjust the size to be 1/TotalRows (percentage size) each iteration. Commented Jul 14, 2014 at 2:31
  • 1
    I'm late to the party, but in Win Forms one can also just use a simple table backed by a list of objects as the data source. Adding and deleting rows is built-in. The complicated thing is the "horizontal growth" of your "scale units". That could be handled by a drop down list column and an Add-button, or a sub-dialogue, if that is acceptable. Commented Jun 20, 2016 at 15:26

4 Answers 4

12

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);
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

4

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[..]

1 Comment

Excellent solution!
2

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.

Comments

1

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

1 Comment

And yes you can use panel for shrinking/growing dynamically

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.