WPF: Dynamic Form aligned by a <Grid> - wpf

I'm building a GUI to edit various XML configuration files based on what values are allowed by their schema. I need a way to display a label for each field in a left column, while displaying a control to edit that field on the right column. If this list of fields weren't dynamic, I'd simply display them in a Grid with two columns. But that doesn't seem so easy with a dynamic list of fields.
The below example shows what I'd like to do, but because DataTemplate can only contain one child, this way won't work. Anyone know of a good workaround that would make this dynamic form layout correctly?
(To simplify the example, I'm using a Uniform grid. I'd probably prefer an approach that drops a Grid in the ControlTemplate (so you can define the columns/rows) - but, I figured this approach got the point across)
<ItemsControl x:Name="FieldItemsCtrl">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="2" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:EditableField}">
<TextBlock Text="{Binding FieldName}" Foreground="{DynamicResource TextBrush}" />
<TextBox Text="{Binding FieldValue}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Thanks!

You can replace what you need from panel with DataTemplate. Check this out:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:System="clr-namespace:System;assembly=mscorlib" >
<Grid>
<ItemsControl>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="FieldName" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0, 0, 14, 0" />
<TextBox Text="FieldValue" Grid.Column="1" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
<System:String>Hello</System:String>
<System:String>World</System:String>
</ItemsControl>
</Grid>
</Page>

Related

Sharing column width between grids defined in a template

I am trying to layout a couple of controls:
The light gray vertical lines are grid splitters. Each line in this table is a grid with three columns, the label on the left, the splitter in the middle and the control on the right, including the text boxes and the image.
I want all columns to be aligned. Here is the data template I use for each line:
<DataTemplate x:Key="PropertyLineTemplate">
<Grid Margin="0,0,0,1">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Name"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition SharedSizeGroup="Value"/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding Title}" />
<GridSplitter Grid.Column="1" Width="2"/>
<SomeUserControl
Grid.Column="2"
Content="{Binding}"
Margin="4, 0, 0, 0"/>
</Grid>
</DataTemplate>
and the template for the view itself:
<DataTemplate DataType="{x:Type vm:PropertiesViewModel}">
<DockPanel>
<!-- Properties -->
<ScrollViewer>
<StackPanel Margin="10">
<ItemsControl ItemsSource="{Binding Properties}"
ItemTemplate="{StaticResource PropertyLineTemplate}" />
</StackPanel>
</ScrollViewer>
</DockPanel>
</DataTemplate>
and the usage of both:
<ContentControl Content="{Binding SelectedItem}" Grid.IsSharedSizeScope="True">
</ContentControl>
where SelectedItem is of type PropertiesViewModel.
I also tried to place Grid.IsSharedSizeScope="True" in each individual grid directly and on the panels containing the control, without effects. How can I achieve that all columns share the width?
Grid.IsSharedSizeScope should be set on the first parent ItemsControl of a Grid, in your case it would be the ItemsControl in the ViewModel template. Then the columns width will be adjusted.
However, if you move one GridSplitter, then it will only adjust that row, not the others (See WPF SharedSizeGroup GridSplitter Issue for a potential solution, if you need resizing.)
Other stuff: you don't need the Stackpanel wrapper around the ItemsControl, since the ItemsControl is the only child. If you need to adjust the panel the ItemsControl uses, then set it on the Property ItemsPanel, like this:
<ItemsControl ItemsSource="{Binding Properties}"
Grid.IsSharedSizeScope="True"
ItemTemplate="{StaticResource PropertyLineTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/> <!-- Or <WrapPanel/> or <UniformGrid/> etc. -->
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
However, the Stackpanel is the default panel of the ItemsControl.
Based on Nautious' answer I've solved the problem by defining two ColumnDefinitions as resources and binding the Width property of all columns in question to the Width property of these 'master column definitions':
<ColumnDefinition x:Key="MasterColumnDefinition" />
<ColumnDefinition x:Key="MasterColumnDefinition2" />
<DataTemplate x:Key="PropertyLineTemplate">
<Grid Margin="0,0,0,1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding Source={StaticResource MasterColumnDefinition}, Path=Width, Mode=TwoWay}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource MasterColumnDefinition2}, Path=Width, Mode=TwoWay}"/>
</Grid.ColumnDefinitions>
...
</Grid>
</DataTemplate>
Might be a little hacky, but it's simple and works. Not, however, that the shared size scope of this solution is the scope in which the resources are defined. I.e. placing these template in the app's resources will lead to all columns in the application being resized synchronously.

Itemscontrol sharing grid between controltemplate and itemtemplate

I have an ItemsControl which has lots of columns and a static header row.
I'm aware of HeaderedItemsControl but I'm not sure it can achieve what I want.
What I currently have is the ControlTemplate serving as the container for the header and the ItemsPresenter, then the ItemTemplate is used to organise the records.
At the moment I've got a duplicate grid in both the content and item templates, and I want to find an elegant solution where I can share the same grid between both templates so I don't have to change two things when editing it around. Mock up of what it look like below, the actual control has many more columns. I've read about IsSharedSizeScope and SharedSizeGroup but I gather this wouldn't work because it only functions when there are two grids sharing a parent element (they would both need to be inside the ControlTemplate)
<ItemsControl ItemsSource="{Binding SomeSource}">
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Col1" />
<TextBlock Grid.Column="1" Text="Col2" />
<TextBlock Grid.Column="2" Text="Col3" />
</Grid>
<ItemsPresenter/>
</StackPanel>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="ItemCol1" />
<TextBlock Grid.Column="1" Text="ItemCol2" />
<TextBlock Grid.Column="2" Text="ItemCol3" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I've probably done something that is bad practice as well so feel free to give me pointers.
I have a feeling you set the value of ItemsSource completely wrong.
It should look like this:
<ItemsControl ItemsSource="{Binding SomeSource}">
When I take your example and run it all works fine to me.
If you wish to share column sizes then yes you could use the SharedSizeGroup. You will have to set the scope to be the ItemsControl.
You got the wrong definition of SharedSizeGroup. It works no matter where the Grids are placed as long they share same one of their parents. Which is the case for you since ItemsControl is the top most control so it is the parent to every control inside.

Stretching the items of an ItemsControl

I'm bulding a small WPF app over here. It's all built strictly with MVVM, using nothing but DataTemplates linked to view model types.
I've seen alot of questions about how to stretch and clip the contents of ListBoxes so that the items fill its parent. After alot of experimenting I managed to get my head around that but now I find myself in the same scenario with the ItemsControl but the same tricks doesn't seem to work.
Here's one of the DataTemplates in use (a simple TextBox). Note how I tried setting the HorizontalAlignment ...
<DataTemplate DataType="{x:Type vm:OneOfMyViewModelTypes}">
<TextBox
Text="{Binding Path=Value}"
HorizontalAlignment="Stretch"
/>
</DataTemplate>
Here's the ItemsControl inside a Grid ...
<Grid Background="Gray">
<Grid.Margin>
<Thickness
Left="{StaticResource ConfigurationDefaultMargin}"
Right="{StaticResource ConfigurationDefaultMargin}"
Bottom="{StaticResource ConfigurationDefaultMargin}"
Top="{StaticResource ConfigurationDefaultMargin}"
/>
</Grid.Margin>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="_key" Width="Auto"/>
<ColumnDefinition SharedSizeGroup="_value" Width="*"/>
</Grid.ColumnDefinitions>
<ItemsControl
Background="DimGray"
Grid.IsSharedSizeScope="True"
ItemsSource="{Binding Path=Configuration, Mode=OneWay}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="_key"/>
<ColumnDefinition SharedSizeGroup="_value"/>
</Grid.ColumnDefinitions>
<TextBlock
Style="{StaticResource ExtensionConfigurationLabel}"
Grid.Column="0"
Margin="5,5,5,0"
Text="{Binding Path=Caption}"
/>
<ContentPresenter
Grid.Column="1"
HorizontalAlignment="Stretch"
Margin="5,5,5,0"
Content="{Binding}"
/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
I've used colors to see how the controls sizes. The Grid is gray and the ItemsControl is dark gray.
This is the result ...
As you can see from the colors the containing Grid stretches while the ItemsControl does not. I did set its HorizontalAlignment property to Stretch but it seems it has no effect. Is there anything else I need do?
Thanks
You have two columns in your main (outer) grid, yet you use only the first column.
The second column uses all the remaining space.

Which ItemControl should I choose and Why?

I have an arbitrary count of ViewModels to display and I want that ViewModels to be displayed in two columns. The first column mostly should display a name/description and the content of the second column varies. I would prefer a Grid-like layout with a Width = "auto" for the first column and Width = "*"for the second column.
I tried a ListView with a GridView but that has some flaws.
I can select the ListViewItems(not necessary)
GridViewColumns have headers(wasted space/ not necessary)
GridViewColumns don’t support Width = "*" like a Grid and therefore doesn’t use all the available space
I could use a Grid, but than I would have to add Grid.RowDefinitions for every ViewModel and wouldn’t be possible to set the Grid.Row Property via Binding.
I also thought about using a StackPanel, but that doesn’t support Templating and I don’t know how to sync the Width for the first column.
So, does anybody know which ItemControl is best suited for this purpose?
Without testing it I would expect an ItemsControl with a data template to be a suitable solution.
You can use Grid.IsSharedSizeScope="true" on a parent element, in this case the ItemsControl, and then synchronize the width of the first column using SharedSizeGroup="somename".
<Grid>
<Grid.Resources>
<DataTemplate x:Key="itemTemplate" DataType="{x:Type MyItemType}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="column1" Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Title}"/>
<!-- More stuff here -->
</Grid>
</DataTemplate>
</Grid.Resources>
<ItemsControl Grid.IsSharedSizeScope="True" ItemTemplate="{StaticResource itemTemplate}" />
</Grid>
I would use a ListBox with a Grid as template like this:
<ListBox ItemsSource="{Binding SomeData}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Data1}" Grid.Column="0"></TextBlock>
<TextBlock Text="{Binding Data1}" Grid.Column="1"></TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Do i have to build a ControlTemplate? Or is there an alternative?

I got a TreeView and want to display the Data nested (not hierarchically). The first level data is called TaskViewModel, the second level data is ArtifactViewModel. I want the ArtifactViewModel horizontal inside a GroupBox which represents TaskViewModel.
I tried different approaches, this is my last one:
<TreeView Name="tvTasks" ItemsSource="{Binding Tasks}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type vm:TaskViewModel}">
<GroupBox Header="{Binding Name, UpdateSourceTrigger=PropertyChanged}">
<StackPanel Orientation="Vertical">
<ListView ItemsSource="{Binding Children}"/>
<TextBlock Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap"/>
</StackPanel>
</GroupBox>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type vm:ArtifactViewModel}">
<Border Background="{Binding Type,Converter={StaticResource Type2Background}}"
Margin="5" BorderBrush="Black" BorderThickness="2" CornerRadius="2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="80"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
TextAlignment="Center" Background="Black" Foreground="White"
Opacity="0.75" Grid.Column="0" Grid.Row="1"/>
</Grid>
</Border>
</DataTemplate>
</TreeView.Resources>
</TreeView>
This looks pretty much like what i want, besides that ArtifactViewModels are shown vertical. And if i click on ArtifactViewModel the tvTasks.SelectedItem doesn't change, because the ListView handels this event. I know that this approach is not the cleverest, but it's just a try.
I looked at this article, but i don't see how to deal with the different objects i want to put in the TreeView. So ... how do i build such a UI?
The main issue you're running into here is that you're nesting multiple controls, each with their own selected items.
If you're planning on showing the data as nested but not hierarchically, don't bother using TreeView. If you want to have one item be selectable at any given point in time, use a ListBox instead.
Now the tricky part is playing around with how you want to lay the items out. Take a look at Bea Stollnitz's example here where she redraws a ListBox as a Canvas. You could do something similar where the ItemsPanelTemplate is a Canvas, and you calculate the x,y coordinates. Alternately, you could use a Grid, and determine the Grid.Row and Grid.Column values.

Resources