¿Cómo construir una grilla de controles en XAML?

I am trying to build a UI in WPF to a specification. The UI is for editing a collection of items. Each item has an editable string property, and also a variable number of read-only strings which the UI needs to display. It might look something like this:

                                  enter image description here

or, depending on data, might have a different number of text label columns:

                                      enter image description here

The number of text columns is completely variable and can vary from one to "lots". The specification calls for the columns to be sized to fit the longest entry (they are invariably very short), and the whole thing should look like a grid. This grid will be contained in a window, stretching the text box horizontally to fit the window.

Importantly, the text boxes can contain multi-line text and will grow automatically to fit the text. The rows below need to be pushed out of the way if that happens.

Pregunta: what would be a good way of doing this in WPF?

Coming from a WinForms background, I am thinking of a TableLayoutPanel, which gets populated directly by code I write. However, I need to do this in WPF. While I could still just get myself a Grid and populate it in code, I would really rather prefer a way that's more in line with how things are done in WPF: namely, define a ViewModel, populate it, and then describe the View entirely in XAML. However, I can't think of a way of describing such a view in XAML.

The closest I can get to this using MVVM and XAML is to use an ItemsControl with one item per row, and use a data template which, in turn, uses another ItemsControl (stacked horizontally this time) for the variable number of labels, followed by the text box. Unfortunately, this can't be made to align vertically in a grid pattern like the spec requires.

preguntado el 24 de agosto de 12 a las 23:08

Hm, what should be displayed if a 1-label entry contains a long string, and 2-label entry 2 short strings? -

@Vlad All entries contain the same number of labels. The longest label determines the width of the column. -

Oh, I see. Let me try with SharedSizeGroup... -

6 Respuestas

This does not map all too well, you could probably use a DataGrid y replantear it to look like this. In other approaches you may need to imperatively add columns or the like to get the layout done right.

(You can hook into AutoGeneratingColumn to set the width of that one writeable column to *)

Respondido 24 ago 12, 23:08

OK. I am happy to add columns, rows and the actual controls imperatively, but unfortunately it's not clear how to add controls in a DataTemplate... - romano starkov

@romkyns: Added an answer there, though i don't recommend it, i would try to do as much as possible in XAML, and as i see it using a DataGrid would be the easiest way to get the variable columns. As the second best option i would probably implement my own panel with adds columns automatically depending on item count (or a complete control at that). - media pensión

Thank you H.B., you help me with every WPF question I have :) - romano starkov

@romkyns: Well, i am almost exclusively active in the WPF tag, so chances are that i will answer some of your WPF questions :) - media pensión

Puedes crear el tuyo propio Panel and then decide on how you want the layout logic to work for the children that are put inside it.

Look at this for inspiration:

You could have a "ColumnCount" property, and then use that within the MeassureOverride y ArrangeOverride to decide when to wrap a child.

Or you could modify this bit of code (I know it's Silverlight code, but it should be close to the same in WPF).

Instead of having the same width for all columns (the default is 1-star "*"), you could add a List/Collection property that records the different column widths sized you want, then in the AutoGrid_LayoutUpdated use those widths to make the ColumnDefinition valores.

Respondido 25 ago 12, 00:08

You've asked for quite a bit, the following code shows how to build a grid with the controls you want that sizes as needed, along with setting up the bindings:

    public void BuildListTemplate(IEnumerable<Class1> myData, int numLabelCols)
        var myGrid = new Grid();

        for (int i = 0; i < myData.Count(); i++)
            myGrid.RowDefinitions.Add(new RowDefinition() { Height= new GridLength(0, GridUnitType.Auto)});
        for (int i = 0; i < numLabelCols; i++)
            myGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(0, GridUnitType.Auto) });
        myGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });
        for (int i = 0; i < myData.Count(); i++)
            for (int j = 0; j < numLabelCols; j++)
                var tb = new TextBlock();
                tb.SetBinding(TextBlock.TextProperty, new Binding("[" + i + "].labels[" + j + "]"));
                tb.SetValue(Grid.RowProperty, i);
                tb.SetValue(Grid.ColumnProperty, j);
                tb.Margin = new Thickness(0, 0, 20, 0);
            var edit = new TextBox();
            edit.SetBinding(TextBox.TextProperty, new Binding("[" + i + "].MyEditString"));
            edit.SetValue(Grid.RowProperty, i);
            edit.SetValue(Grid.ColumnProperty, numLabelCols);
            edit.AcceptsReturn = true;
            edit.TextWrapping = TextWrapping.Wrap;
            edit.Margin = new Thickness(0, 0, 20, 6);
       contentPresenter1.Content = myGrid;

A Quick Explanation of the above All it is doing is creating the grid, defines rows for the grid; and a series of columns for the grid that auto size for the content.

Then it simply generates controls for each data point, sets the binding path, and assigns various other display attributes along with setting the correct row/column for the control.

Finally it puts the grid in a contentPresenter that has been defined in the window xaml in order to show it.

Now all you need do is create a class with the following properties and set the data context of the contentPresenter1 to a list of that object:

public class Class1
    public string[] labels { get; set; }
    public string MyEditString { get; set; }

just for completeness here is the window xaml and constructor to show hooking it all up:

<Window x:Class="WpfApplication1.MainWindow"
    Title="MainWindow" Height="350" Width="525">
<ContentPresenter Name="contentPresenter1"></ContentPresenter>

    public MainWindow()

        var data = new List<Class1>();

        data.Add(new Class1() { labels = new string[] {"the first", "the second", "the third"}, MyEditString = "starting text"});
        data.Add(new Class1() { labels = new string[] { "col a", "col b" }, MyEditString = "<Nothing>" });

        BuildListTemplate(data, 3);
        DataContext = data;

You can of course use other methods such as a listview and build a gridview for it (I'd do this if you have large numbers of rows), or some other such control, but given your specific layout requirements probably you are going to want this method with a grid.

EDITAR: Just spotted that you're looking for a way of doing in xaml - tbh all I can say is that I don't think that with the features you're wanting that it is too viable. If you didn't need to keep things aligned to dynamically sized content on seperate rows it would be more viable... But I will also say, don't fear code behind, it has it's place when creating the ui.

Respondido 25 ago 12, 08:08

It would have been sufficient to say that you think I should populate the Grid in code :) Unfortunately, there is a problem with that I don't know how to solve... - romano starkov

well doing is learned (for me ;) : I actually completely missed the "in xaml" part to begin with - just to elaborate further, the example above I gave is slightly tied to the data, but with a little reflection, perhaps a few dependency properties and monitoring the datacontext changed event (or there abouts) I believe you could have it operating quite similar to if the whole thing was in xaml - I'd also put it together as a usercontrol - Disclaimer: I'm relatively new to WPF / c#. - Alternador

Doing it in the code-behind is really not a WPFish(wpf way). Here I offer you my solution, which looks nice imo.

0) Before starting, you need GridHelpers. Those make sure you can have dynamically changing rows/columns. You can find it with a little bit of google:

How can I dynamically add a RowDefinition to a Grid in an ItemsPanelTemplate?

Before actually implementing something, you need to restructure your program a little. You need new structure "CustomCollection", which will have:

  • RowCount - how many rows are there(implement using INotifyPropertyChanged)
  • ColumnCount - how many columns are there(implement using INotifyPropertyChanged)
  • ActualItems - Your own collection of "rows/items"(ObservableCollection)

1) Start by creating an ItemsControl that holds Grid. Make sure Grid RowDefinitions/ColumnDefinitions are dynamic. Apply ItemContainerStyle.

      ItemsSource="{Binding Collection.ActualItems, 
        Converter={StaticResource presentationConverter}">
             local:GridHelpers.RowCount="{Binding Collection.RowCount}"
             local:GridHelpers.StarColumns="{Binding Collection.ColumnCount, 
               Converter={StaticResource subtractOneConverter}"
             local:GridHelpers.ColumnCount="{Binding Collection.ColumnCount}" />
         <Style TargetType="{x:Type FrameworkElement}">
           <Setter Property="Grid.Row" Value="{Binding RowIndex}"/>
           <Setter Property="Grid.Column" Value="{Binding ColumnIndex}"/>

The only thing left to do: implement presentationConverter which converts your Viewmodel presentation to View presentation. (Read: http://wpftutorial.net/ValueConverters.html)

The converter should give back a collection of items where each "label" or "textbox" is a seperate entity. Each entity should have RowIndex and ColumnIndex.

Here is entity class:

public class SingleEntity
   ..RowIndex property..
   ..ColumnIndex property..
   ..ContentProperty..  <-- This will either hold label string or TextBox binded property.

Note that ContentType is an enum which you will bind against in ItemsTemplate to decide if you should create TextBox or Label.

This might seem like a quite lengthy solution, but it actually is nice for few reasons:

  • The ViewModel does not have any idea what is going on. This is purely View problem.
  • Everything is dynamic. As soon you add/or remove something in ViewModel(assuming everything is properly implemented), your ItemsControl will retrigger the Converter and bind again. If this is not the case, you can set ActualItems=null and then back.

Si usted tiene alguna pregunta, hágamelo saber.

contestado el 23 de mayo de 17 a las 12:05

Well, the simple yet not not very advanced way would be to fill the UI dynamically in the code-behind. This seems to be the easiest solution, and it more or less matches your winforms experience.

If you want to do it in a MVVM way, you should perhaps use ItemsControl, set the collection of items as its ItemsSource, and define a DataTemplate for your collection item type.

Yo tendría el DataTemplate con algo como eso:

<Window x:Class="SharedSG.MainWindow"
        Title="MainWindow" Height="350" Width="525">
        <DataTemplate DataType="{x:Type app:LabelVM}">
                    <ColumnDefinition SharedSizeGroup="G1"/>
                    <ColumnDefinition SharedSizeGroup="G2"/>
                    <ColumnDefinition MinWidth="40" Width="*"/>
                <Label Content="{Binding L1}" Grid.Column="0"/>
                <Label Content="{Binding L2}" Grid.Column="1"/>
                <TextBox Grid.Column="2"/>
    <Grid Grid.IsSharedSizeScope="True">
        <ItemsControl ItemsSource="{Binding}"/>

Respondido 24 ago 12, 23:08

Only because every other time I tried to do things the WinForms way, it was invariably ugly, hacky, and extremely hard (e.g. jugando con el selected item in a TreeView). - romano starkov

Thanks. Could you please elaborate how I would get the vertical alignment I'm after using ItemsControl/DataTemplate? - romano starkov

@romkyns: one more update :) This achieves the vertical alignment if all the types in the collection are the same. - Vlad

I don’t understand how the posted code sample addresses the question. It seems to assume only one column of text labels. The question explicitly states there could be any number of them, depending on the specific object displayed. The core of the question is what the XAML would need to look like if you don’t know the number of columns until runtime? - timwi

Vlad, the number of labels is variable. There is no Label in the view model. There is a collection of labels. Moreover, this template generates a grid for each row, which still doesn't get the vertical alignment I need... - romano starkov

You're probably way past this issue, but I had a similar issue recently and I got it to work surprisingly well in xaml, so I thought I'd share my solution.

The major downside is that you have to be willing to put an upper-bound on what "lots" of labels means. If lots can mean 100s, this won't work. If lots will definitely be less than the number of times you're willing to type Ctrl+V, you might be able to get this to work. You also have to be willing to put all the labels into a single ObservableCollection property in your view model. It sounded to me in your question that you already tried that out anyway though.

I takes advantage of AlternationIndex to get the index of the label and assign it to a column. Think I learned that from aquí. If an item has < x labels the extra columns won't get in the way. If an item has > x labels, the labels will start stacking on top of each other.

<!-- Increase AlternationCount and RowDefinitions if this template breaks -->
<ItemsControl ItemsSource="{Binding Labels}" IsTabStop="False" AlternationCount="5">
                 <TextBlock Text="{Binding}"/>
            <Style TargetType="{x:Type ContentPresenter}">
                <Setter Property="Grid.Column" 
                        Value="{Binding RelativeSource={RelativeSource Self}, 
                <Grid IsItemsHost="True">
                        <ColumnDefinition SharedSizeGroup="A"/>
                        <ColumnDefinition SharedSizeGroup="B"/>
                        <ColumnDefinition SharedSizeGroup="C"/>
                        <ColumnDefinition SharedSizeGroup="D"/>
                        <ColumnDefinition SharedSizeGroup="E"/>

contestado el 23 de mayo de 17 a las 13:05

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.