6
\$\begingroup\$

In my real life example I am at a layout stage and before I actually move any further I would like to review my current approach. I am still learning about UI, XAML on WPF technology and I guess I haven't gotten my head around all the possible approaches yet.

I have built this very simple SSCCE to better demonstrate what I mean:

XAML

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        Title="MainWindow" Width="200" MinWidth="150" SizeToContent="Height">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>

        <Label Grid.Row="0" Grid.Column="0">First Name</Label>
        <Label Grid.Row="1" Grid.Column="0">Last Name</Label>
        <Label Grid.Row="2" Grid.Column="0">Address</Label>
        <Label Grid.Row="3" Grid.Column="0">Phone</Label>

        <TextBox Grid.Row="0" Grid.Column="1"></TextBox>
        <TextBox Grid.Row="1" Grid.Column="1"></TextBox>
        <TextBox Grid.Row="2" Grid.Column="1" Height="Auto" TextWrapping="Wrap" AcceptsReturn="True"></TextBox>
        <TextBox Grid.Row="3" Grid.Column="1"></TextBox>
    </Grid>
</Window>

Here is what it looks like:

sscce

Assume I will be having a list of 50+ "rows" in a form that will be either populated by a User or deserialized from an XML. At some point I may be asked to insert an extra row in between the current elements.

I can already see a potential problem of having to adjust the Grid.Row attribute's value at the point of inserting a new item until the last one.

For example, I am inserting a new element before the current Grid.Row 3 which would require me to manually shift all the attached properties of Grid.Row on all the remaining rows (say 50 - 3 ). At this point it just does not sound like I have had a well designed window/xaml from the start.

How can I overcome the problem of adding extra rows to this grid? Maybe I should start over with a different Content Control?

I am not looking for a complete solution just some insights and tips as I'm having so much fun actually digging and implementing based on tips rather than copy-pasting others code.

My alternative suggestion:

I have imported the xmlns:sys="clr-namespace:System;assembly=mscorlib" and was thinking of using a List and then filling each ListBoxItem with Labels and Textbox as well as other controls like Grid etc but I don't think I can use more than 1 control inside the <ListBoxItem> therefore I am not sure this is a good approach.

What I do like about it is the syntax of inserting:

<ListBox>
    <ListBoxItem>
        <!-- first item and no need to explicitly numerate -->
    </ListBoxItem>
    <ListBoxItem>
        <!-- second item -->
    </ListBoxItem>
    <ListBoxItem>
        <!-- newly inserted becomes 3rd -->
    </ListBoxItem>
    <ListBoxItem>
        <!-- was third but now is 4th -->
    </ListBoxItem>
</ListBox>

Another approach I am considering is

<StackPanel>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition Width="2*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Label>boo</Label>
        <TextBox Grid.Column="1"></TextBox>
    </Grid>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition Width="2*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Label>foo</Label>
        <TextBox Grid.Column="1"></TextBox>
    </Grid>
</StackPanel>

As it would rather be easy to insert new items and no explicit numeration required, but this is kind of a lot of code as for each StackPanels' item I would need to create a <Grid> with Rows and Columns definitions.

\$\endgroup\$
11
+50
\$\begingroup\$

WPF readability

To keep your xaml looking clean, I reccommend using self closing tags

<RowDefinition />

vs

<RowDefinition></RowDefinition>

Explicitly set properties instead of using the > content area < of a tag. Doing such more explicitly shows what the content is used for.

<Label Grid.Row="0" Grid.Column="0" Content="First Name" />

vs

<Label Grid.Row="0" Grid.Column="0">First Name</Label>

UI Layout

I am unsure if the below is a mistake or not, but it does produce a very strange layout when the control is re-sized.

<Grid.RowDefinitions>
    <RowDefinition />
    <RowDefinition />
    <RowDefinition Height="Auto" />
    <RowDefinition />
</Grid.RowDefinitions>

I recommend using Height="Auto" on all your rows. I also recommend you use Width="Auto" on your first column to have the TextBoxes stretch to butt up against the longest Label.


Your Address TextBox has its Height set to Auto, but this is unnecessary. The Control will naturally fill up its Container i.e. the row it is in, and that Row has a Height set to Auto.

Future Changes

If you know you are going to have to add more rows in the future, you can always add more rows right now with a Height="Auto", and if those rows have nothing in them, then they collapse flat. This isn't ideal, but really you should just have your UI planned out ahead of time.

Writing a spec for your program may be useful to properly account for all features you plan to add. Also xaml is super easy to generate, you could always write a script to add multiple new rows faster than actually writing the new rows in.

Don't hard-code a UI that needs to be dynamic

Assume I will be having a list of 50+ "rows" in a form that will be either populated by a User or deserialized from an XML. At some point I may be asked to insert an extra row in between the current elements.

You should not have to hard-code a UI which is that massive and could change any day, and require you to make code changes. That is not practical.

Instead you should dynamically generate the form after you deserialize the data, and determine which rows you need to create.

Alternatively you could create a config file which would contain simple UI layout information like

<row Header="First Name" ContentType="TextBox" PropertyInDataToLinkTo="fName" />

Then dynamically generate the UI based off of this information, which you could quickly change in production without having to recompile. Taking this above approach, you would store all your form data, or deserialized data in a Dictionary<string, object> which will easily allow for dynamic name addressable data.

Edit: Example of dynamic row content

Here I created a super simple xaml file, which could be just one line, but I added a button at the bottom to display the data.

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition Height="20" />
    </Grid.RowDefinitions>
    <Grid Grid.Row="0" Name="grid" />
    <Button Grid.Row="1" Content="Submit" Click="Button_Click" />
</Grid>

Here in this implementation where my <Grid Grid.Row="0" Name="grid" /> is, is where all the dynamic data will get poured into.

public partial class MainWindow : Window
{
    private Dictionary<string, string> _formData;
    private List<Tuple<TextBlock, TextBox, string>> _formLayout;

    public MainWindow()
    {
        InitializeComponent();

        //Initialize this collection for easy access to data later
        _formLayout = new List<Tuple<TextBlock, TextBox, string>>();

        //Load this information from a file <Header Text, Data Name>
        List<Tuple<string, string>> layoutFromFile = QuickXMLReader.Read("config.cfg").First()
            .Select(item => Tuple.Create(item["Header"], item["DataName"])).ToList();

        //Create entries for this data 
        _formData = new Dictionary<string, string>();
        foreach (var val in layoutFromFile)
            if (!_formData.ContainsKey(val.Item2))
                _formData.Add(val.Item2, string.Empty); //use serialized data if it exists

        //Add content to Grid
        grid.ColumnDefinitions.Insert(0, new ColumnDefinition());
        grid.ColumnDefinitions.Insert(0, new ColumnDefinition() { Width = GridLength.Auto });
        for (int i = 0; i < layoutFromFile.Count; i++)
        {
            var val = layoutFromFile[i];
            grid.RowDefinitions.Insert(grid.RowDefinitions.Count, new RowDefinition() { Height = GridLength.Auto });

            var textBlock = new TextBlock() { Text = val.Item1 };
            grid.Children.Add(textBlock);
            Grid.SetColumn(textBlock, 0);
            Grid.SetRow(textBlock, i);

            var textBox = new TextBox() { AcceptsReturn = true, Text = _formData[val.Item2] };
            grid.Children.Add(textBox);
            Grid.SetColumn(textBox, 1);
            Grid.SetRow(textBox, i);

            _formLayout.Add(Tuple.Create(textBlock, textBox, val.Item2));
        }
    }

    private void CommitData()
    {
        foreach(var val in _formLayout) { _formData[val.Item3] = val.Item2.Text; }
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        CommitData();
        MessageBox.Show(string.Join(Environment.NewLine, _formData.Select(item => string.Format("{0} = \"{1}\"", item.Key, item.Value))));
    }
}

Because you don't have my QuickXMLReader, you can replace that code with this. You can really generate this information anyway you'd like.

//Load this information from a file <Header Text, Data Name>
List<Tuple<string, string>> layoutFromFile = new List<Tuple<string, string>>() { 
    Tuple.Create("First Name", "fName"),
    Tuple.Create("Last Name", "lName"),
    Tuple.Create("Address", "address"),
    Tuple.Create("Phone", "phone")};

That would replace my xml file with these entries

<Entries>
  <Entry Header="First Name" DataName="fName" />
  <Entry Header="Last Name" DataName="lName" />
  <Entry Header="Address" DataName="address" />
  <Entry Header="Phone" DataName="phone" />
</Entries>

This code, dynamically generates all the rows as you can see in the screenshot below enter image description here

When you press the button it shows the submitted data collected in the back in the _formData dictionary.

enter image description here

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy