The Problem
I have a C# window with some text fields and buttons on it. It starts out similar to this:
When the user clicks that "+ Add Machine Function" button, I need to create a new row of controls and move the button below those:
If the user clicks "+Add Scale Unit" the program needs to add some controls to the right:
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?
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);
}
}
}
}
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
Add a FlowLayoutPanel
to the main form to contain all the user controls (and to simplify their positioning)
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).
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[..]
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With