ItemsControl - Grid child elements auto resize - wpf

I'm using Rachel Lim's GridHelper to get dynamic number of rows. What I wanted to achieve is to have each row displayed one below another (done), to be able to resize them (done - using GridSplitter) and to have content resized proportionally to the screen size.
Result:
What I would like to have:
Xaml:
<Grid>
<ItemsControl ItemsSource="{Binding RowSource}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid local:GridHelper.RowCount="{Binding RowCount}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Grid.Row" Value="{Binding RowNumber}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<DataGrid>
<DataGrid.Columns>
<DataGridTextColumn Header="Col 1" />
<DataGridTextColumn Header="Col 2" />
<DataGridTextColumn Header="Col 3" />
</DataGrid.Columns>
</DataGrid>
<Button Grid.Column="1" Content="Btn" />
</Grid>
<GridSplitter Height="5" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Grid.Row="0" ResizeDirection="Rows" ResizeBehavior="CurrentAndNext"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
ViewModel:
internal class MyViewModel
{
public ObservableCollection<RowInfo> RowSource { get; set; }
public int RowCount { get { return RowSource.Count; } }
public MyViewModel()
{
RowSource = new ObservableCollection<RowInfo>()
{
new RowInfo() { RowNumber = 0 },
new RowInfo() { RowNumber = 1 },
new RowInfo() { RowNumber = 2 }
};
}
}
RowInfo:
public class RowInfo
{
public int RowNumber { get; internal set; }
}

I think your approach is all wrong. You cannot use an ItemsControl, as the GridSplitter items need to be at the ItemsPanel level rather than in the DataTemplate - otherwise, it won't work.
You are better off using a custom behavior on the Grid Itself - see example code below:
public class GridAutoRowChildBehavior : Behavior<Grid>
{
public static readonly DependencyProperty ItemTemplateProperty =
DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(GridAutoRowChildBehavior),
new PropertyMetadata(null, OnGridPropertyChanged));
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(object), typeof(GridAutoRowChildBehavior),
new PropertyMetadata(null, OnGridPropertyChanged));
private static void OnGridPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((GridAutoRowChildBehavior) d).ResetGrid();
}
private void ResetGrid()
{
var source = ItemsSource as IEnumerable;
if (source == null || ItemTemplate == null)
return;
AssociatedObject.Children.Clear();
AssociatedObject.RowDefinitions.Clear();
var count = 0;
foreach (var item in source)
{
var content = new ContentPresenter
{
ContentTemplate = ItemTemplate,
Content = item
};
var splitter = new GridSplitter
{
Height = 5,
VerticalAlignment = VerticalAlignment.Bottom,
HorizontalAlignment = HorizontalAlignment.Stretch
};
AssociatedObject.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
Grid.SetRow(content,count);
Grid.SetRow(splitter,count);
AssociatedObject.Children.Add(content);
AssociatedObject.Children.Add(splitter);
count++;
}
}
public DataTemplate ItemTemplate
{
get { return (DataTemplate) GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public object ItemsSource
{
get { return GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
}
Then in your XAML you code it up like this:
<Grid>
<i:Interaction.Behaviors>
<local:GridAutoRowChildBehavior ItemsSource="{Binding RowsSource}">
<local:GridAutoRowChildBehavior.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<DataGrid>
<DataGrid.Columns>
<DataGridTextColumn Header="Col 1" />
<DataGridTextColumn Header="Col 2" />
<DataGridTextColumn Header="Col 3" />
</DataGrid.Columns>
</DataGrid>
<Button Grid.Column="1" Content="Btn" />
</Grid>
</DataTemplate>
</local:GridAutoRowChildBehavior.ItemTemplate>
</local:GridAutoRowChildBehavior>
</i:Interaction.Behaviors>
</Grid>
I have tested this and it works exactly as you need.
The only additional thing you need to do is add the Nuget package Systems.Windows.Interactivity.WPF to your project

Use star-sizing for the RowDefintions that you created in the GridHelper class:
public static void RowCountChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is Grid) || (int)e.NewValue < 0)
return;
Grid grid = (Grid)obj;
grid.RowDefinitions.Clear();
for (int i = 0; i < (int)e.NewValue; i++)
grid.RowDefinitions.Add(
new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) }); //<--
SetStarRows(grid);
}
And set the Height of your first RowDefinition to *:
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Background="Yellow">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<DataGrid>
<DataGrid.Columns>
<DataGridTextColumn Header="Col 1" />
<DataGridTextColumn Header="Col 2" />
<DataGridTextColumn Header="Col 3" />
</DataGrid.Columns>
</DataGrid>
<Button Grid.Column="1" Content="Btn" />
</Grid>
<GridSplitter Height="5" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Grid.Row="0" ResizeDirection="Rows" ResizeBehavior="CurrentAndNext"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>

Related

ListView ItemsPanelTemplate with horizontal orientation, how to Identify ListViewItems on first row?

First of all I am working with MVVM / WPF / .Net Framework 4.6.1
I have a ListView configured with ItemsPanelTemplate in horizontal orientation that displays items from a DataTemplate. This setup allows me to fit as many items inside the Width of the ListView (the witdth size is the same from the Window), and behaves responsively when I resize the window.
So far everything is fine, now I just want to Identify what items are positioned on the first row, including when the window get resized and items inside the first row increase or decrease.
I merely want to accomplish this behavior because I would like to apply a different template style for those items (let's say a I bigger image or different text color).
Here below the XAML definition for the ListView:
<ListView x:Name="lv"
ItemsSource="{Binding Path = ItemsSource}"
SelectedItem="{Binding Path = SelectedItem}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<Grid Width="180" Height="35">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32"
VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding IconPathName}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
HorizontalAlignment="Left" VerticalAlignment="Top"
Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
BTW: I already did a work around where I am getting the Index from each ListViewItem and calculating against the Width of the Grid inside the DataTemplate that is a fixed value of 180, but unfortunately it did not work as I expected since I had to use a DependencyProperty to bind the ActualWidth of the of the ListView to my ViewModel and did not responded very well when I resized the window.
I know I am looking for a very particular behavior, but if anyone has any suggestions about how to deal with this I would really appreciate. Any thoughts are welcome even if you think I should be using a different control, please detail.
Thanks in advance!
You shouldn't handle the layout in any view model. If you didn't extend ListView consider to use an attached behavior (raw example):
ListBox.cs
public class ListBox : DependencyObject
{
#region IsAlternateFirstRowTemplateEnabled attached property
public static readonly DependencyProperty IsAlternateFirstRowTemplateEnabledProperty = DependencyProperty.RegisterAttached(
"IsAlternateFirstRowTemplateEnabled",
typeof(bool), typeof(ListView),
new PropertyMetadata(default(bool), ListBox.OnIsEnabledChanged));
public static void SetIsAlternateFirstRowTemplateEnabled(DependencyObject attachingElement, bool value) => attachingElement.SetValue(ListBox.IsAlternateFirstRowTemplateEnabledProperty, value);
public static bool GetIsAlternateFirstRowTemplateEnabled(DependencyObject attachingElement) => (bool)attachingElement.GetValue(ListBox.IsAlternateFirstRowTemplateEnabledProperty);
#endregion
private static void OnIsEnabledChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
{
if (!(attachingElement is System.Windows.Controls.ListBox listBox))
{
return;
}
if ((bool)e.NewValue)
{
listBox.Loaded += ListBox.Initialize;
}
else
{
listBox.SizeChanged -= ListBox.OnListBoxSizeChanged;
}
}
private static void Initialize(object sender, RoutedEventArgs e)
{
var listBox = sender as System.Windows.Controls.ListBox;
listBox.Loaded -= ListBox.Initialize;
// Check if items panel is WrapPanel
if (!listBox.TryFindVisualChildElement(out WrapPanel panel))
{
return;
}
listBox.SizeChanged += ListBox.OnListBoxSizeChanged;
ListBox.ApplyFirstRowDataTemplate(listBox);
}
private static void OnListBoxSizeChanged(object sender, SizeChangedEventArgs e)
{
if (!e.WidthChanged)
{
return;
}
var listBox = sender as System.Windows.Controls.ListBox;
ListBox.ApplyFirstRowDataTemplate(listBox);
}
private static void ApplyFirstRowDataTemplate(System.Windows.Controls.ListBox listBox)
{
double calculatedFirstRowWidth = 0;
var firstRowDataTemplate = listBox.Resources["FirstRowDataTemplate"] as DataTemplate;
foreach (FrameworkElement itemContainer in listBox.ItemContainerGenerator.Items
.Select(listBox.ItemContainerGenerator.ContainerFromItem).Cast<FrameworkElement>())
{
calculatedFirstRowWidth += itemContainer.ActualWidth;
if (itemContainer.TryFindVisualChildElement(out ContentPresenter contentPresenter))
{
if (calculatedFirstRowWidth > listBox.ActualWidth - listBox.Padding.Right - listBox.Padding.Left)
{
if (contentPresenter.ContentTemplate == firstRowDataTemplate)
{
// Restore the default template of previous first row items
contentPresenter.ContentTemplate = listBox.ItemTemplate;
continue;
}
break;
}
contentPresenter.ContentTemplate = firstRowDataTemplate;
}
}
}
}
Helper Extension Method
/// <summary>
/// Traverses the visual tree towards the leafs until an element with a matching element type is found.
/// </summary>
/// <typeparam name="TChild">The type the visual child must match.</typeparam>
/// <param name="parent"></param>
/// <param name="resultElement"></param>
/// <returns></returns>
public static bool TryFindVisualChildElement<TChild>(this DependencyObject parent, out TChild resultElement)
where TChild : DependencyObject
{
resultElement = null;
if (parent is Popup popup)
{
parent = popup.Child;
if (parent == null)
{
return false;
}
}
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is TChild child)
{
resultElement = child;
return true;
}
if (childElement.TryFindVisualChildElement(out resultElement))
{
return true;
}
}
return false;
}
Usage
<ListView x:Name="lv"
ListBox.IsAlternateFirstRowTemplateEnabled="True"
ItemsSource="{Binding Path = ItemsSource}"
SelectedItem="{Binding Path = SelectedItem}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.Resources>
<DataTemplate x:Key="FirstRowDataTemplate">
<!-- Draw a red border around first row items -->
<Border BorderThickness="2" BorderBrush="Red">
<Grid Width="180" Height="35">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32"
VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding IconPathName}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
HorizontalAlignment="Left" VerticalAlignment="Top"
Text="{Binding Name}" />
</Grid>
</Border>
</DataTemplate>
</ListView.Resources>
<ListView.ItemTemplate>
<DataTemplate>
<Grid Width="180" Height="35">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32"
VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding IconPathName}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
HorizontalAlignment="Left" VerticalAlignment="Top"
Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Remarks
If the visual tree itself will not change for the first row, consider to add a second attached property to the ListBox class (e.g., IsFirstRowItem) which you would set on the ListBoxItems. You can then use a DataTrigger to modify the control properties to change the appearance. This will very likely increase the performance too.

How to get datacontext of an event emitting item control?

I have a bunch of buttons being render inside an items control. I want to get access to the data context of the clicked button. How can I achieve that?
Model:
public class RunYear
{
public RunYear(int year)
{
Year = year;
Months = new Month[3];
}
public int Year { get; set; }
public Month[] Months { get; set; }
}
public class Month
{
public int ColumnIndex { get; set; }
public string MonthName { get; set; }
// some other props
}
Code behind:
public partial class MainWindow : Window
{
private ObservableCollection<RunYear> _years = new ObservableCollection<RunYear>();
public ObservableCollection<RunYear> Years { get{return _years; } }
public MainWindow()
{
DataContext = this;
InitializeComponent();
GenerateData();
}
private void GenerateData()
{
for (int i = 2010; i < 2015; i++)
{
var runYear = new RunYear(i);
runYear.Months[0] = new Month() { ColumnIndex = 0, MonthName = $"Jan {i}" };
runYear.Months[1] = new Month() { ColumnIndex = 1, MonthName = $"Feb {i}" };
runYear.Months[2] = new Month() { ColumnIndex = 2, MonthName = $"Mar {i}" };
Years.Add(runYear);
}
}
public void OnClick(object sender, RoutedEventArgs args)
{
// how do I get the databound item?
var cp = sender as ContentPresenter; //doesn't work
var vm = cp?.Content as Month;
}
}
XAML:
<Grid>
<ItemsControl Name="icYears" ItemsSource="{Binding Years}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="70" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="75"/>
</Grid.RowDefinitions>
<DockPanel Grid.Column="0" Grid.Row="0" >
<TextBox IsReadOnly="True" TextAlignment="Center" Text="{Binding Year}" />
</DockPanel>
<ItemsControl Grid.Column="1" Name="icMonths" ItemsSource="{Binding Months}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="75"></RowDefinition>
</Grid.RowDefinitions>
</Grid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Grid.Column" Value="{Binding ColumnIndex}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Click="OnClick" Content="{Binding MonthName}" Padding="2" Margin="2"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Do you mean like this?
private void Button_Click(object sender, RoutedEventArgs e)
{
var vm = (sender as FrameworkElement).DataContext;

How to get a DataGrid's scrollbars to show up when it is in an Expander?

I have a DataGrid that is in one of a series of Expanders in a resizable window. When the DataGrid rows are loaded, the DataGrid extends off the bottom of the window without any scrollbars.
I've reduced the issue to the simplest elements I could below
I've tried putting the DataGrid in a separate ScrollViewer but had the same issue.
I also need the other two Expanders to remain visible in the window and not be pushed off the edge. I had a little success putting the three Expanders in a DockPanel, but the DataGrid still expanded to fill the entire window, pushing the other Expanders out of view.
XAML
<Window x:Class="WPFTestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPFTestApp"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid Name="root">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Expander Grid.Row="0" Name="Expander1" Expanded="Expander_Expanded">
<Expander.Header>Expander</Expander.Header>
<Expander.Content>
<ScrollViewer>
<DataGrid Grid.Row="1" Name="dataGrid">
<DataGrid.Columns>
<DataGridTextColumn Header="A" Binding="{Binding A}" />
<DataGridTextColumn Header="B" Binding="{Binding B}" />
<DataGridTextColumn Header="C" Binding="{Binding C}" />
<DataGridTextColumn Header="D" Binding="{Binding D}" />
</DataGrid.Columns>
</DataGrid>
</ScrollViewer>
</Expander.Content>
</Expander>
<Expander Grid.Row="1" Name="Expander2" Expanded="Expander_Expanded">
<Expander.Content>
<TextBlock>...<LineBreak/>...<LineBreak/>...<LineBreak/>...<LineBreak/>...<LineBreak/>...</TextBlock>
</Expander.Content>
</Expander>
<Expander Grid.Row="2" Name="Expander3" Expanded="Expander_Expanded">
<Expander.Content>
<TextBlock>...<LineBreak/>...<LineBreak/>...<LineBreak/>...<LineBreak/>...<LineBreak/>...</TextBlock>
</Expander.Content>
</Expander>
</Grid>
</Window>
Codebehind
using System;
using System.Windows;
namespace WPFTestApp {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public class Data {
public Guid A { get; set; }
public Guid B { get; set; }
public Guid C { get; set; }
public Guid D { get; set; }
public Data() {
A = Guid.NewGuid();
B = Guid.NewGuid();
C = Guid.NewGuid();
D = Guid.NewGuid();
}
}
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
for (int x = 0; x < 20; x++) {
dataGrid.Items.Add(new Data());
}
}
private void Expander_Expanded(object sender, RoutedEventArgs e) {
if (Expander1 != sender)
Expander1.IsExpanded = false;
if (Expander2 != sender)
Expander2.IsExpanded = false;
if (Expander3 != sender)
Expander3.IsExpanded = false;
}
}
}
Remove the ScrollViewer element around the DataGrid and specify a height (either a fixed size or a star-sized one) for the RowDefinitions:
<Grid Name="root">
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Expander Grid.Row="0" Name="Expander1" Expanded="Expander_Expanded">
<Expander.Header>Expander</Expander.Header>
<Expander.Content>
<DataGrid Grid.Row="1" Name="dataGrid">
<DataGrid.Columns>
<DataGridTextColumn Header="A" Binding="{Binding A}" />
<DataGridTextColumn Header="B" Binding="{Binding B}" />
<DataGridTextColumn Header="C" Binding="{Binding C}" />
<DataGridTextColumn Header="D" Binding="{Binding D}" />
</DataGrid.Columns>
</DataGrid>
</Expander.Content>
</Expander>
<Expander Grid.Row="1" Name="Expander2" Expanded="Expander_Expanded">
<Expander.Content>
<TextBlock>...<LineBreak/>...<LineBreak/>...<LineBreak/>...<LineBreak/>...<LineBreak/>...</TextBlock>
</Expander.Content>
</Expander>
<Expander Grid.Row="2" Name="Expander3" Expanded="Expander_Expanded">
<Expander.Content>
<TextBlock>...<LineBreak/>...<LineBreak/>...<LineBreak/>...<LineBreak/>...<LineBreak/>...</TextBlock>
</Expander.Content>
</Expander>
</Grid>
You won't get any vertical scrollbars when the Height is set to Auto because then the Expander is considered to have an infinite height.
Changing the RowDefinitions to
<Grid.RowDefinitions>
<RowDefinition Name="gridRowEx1" Height="Auto" />
<RowDefinition Name="gridRowEx2" Height="Auto" />
<RowDefinition Name="gridRowEx3" Height="Auto" />
</Grid.RowDefinitions>
And changing the Expander_Expanded function to
private void Expander_Expanded(object sender, RoutedEventArgs e) {
switch (((Expander)sender).Name) {
case "Expander1":
Expander2.IsExpanded = false;
Expander3.IsExpanded = false;
gridRowEx1.Height = new GridLength(1.0, GridUnitType.Star);
gridRowEx2.Height = new GridLength(1.0, GridUnitType.Auto);
gridRowEx3.Height = new GridLength(1.0, GridUnitType.Auto);
break;
case "Expander2":
Expander1.IsExpanded = false;
Expander3.IsExpanded = false;
gridRowEx1.Height = new GridLength(1.0, GridUnitType.Auto);
gridRowEx2.Height = new GridLength(1.0, GridUnitType.Star);
gridRowEx3.Height = new GridLength(1.0, GridUnitType.Auto);
break;
case "Expander3":
Expander1.IsExpanded = false;
Expander2.IsExpanded = false;
gridRowEx1.Height = new GridLength(1.0, GridUnitType.Auto);
gridRowEx2.Height = new GridLength(1.0, GridUnitType.Auto);
gridRowEx3.Height = new GridLength(1.0, GridUnitType.Star);
break;
}
}
Gives me exactly what I needed

WPF Databinding not working in both directions when DataGrid bound to Observable Collection of Properties with Sub properties

Excuse the wordy title, I'm having trouble with a succinct description. If I could come up with one, I could probably Google the right answer!
I am binding my DataGrid to an ObservableCollection of properties that themselves have properties. My grid is populated just fine, but when I edit the grid the changes are not getting back to my model.
I have an ObservableCollection
Normally, you'd just have some properties of MarriedCoupleRow, but I actually have something slightly more complicated. Each MarriedCoupleRow has some propties (Male, Female) which in turn expose properties (Height, Weight, Information). It's this Information that can be edited. Again, I can populate the grid just fine, but the setter property of Information is not hit when you edit the cell and tab off (or leave).
I'd appreciate any pointers or references, including how to better word my title!
Here's the simple code:
public class XMLDemoViewModel : ViewModelBase
{
public XMLDemoViewModel()
{
_rows = new ObservableCollection<MarriedCoupleRow>();
// create some data....
for (uint i = 0; i < 2;i++)
{
MarriedCoupleRow row = new MarriedCoupleRow();
row.Male = new HumanData();
row.Male.Height = (70 + i*5).ToString();
row.Male.Weight = 150+(i*30+1);
row.Male.Information = row.Male.Height + " " + row.Male.Weight;
row.Female = new HumanData();
row.Female.Height = (60 +i*3).ToString();
row.Female.Weight = 120+(i*10+5);
row.Female.Information = row.Female.Height + " " + row.Female.Weight;
_rows.Add(row);
}
}
#region Fields
private ObservableCollection<MarriedCoupleRow> _rows = null;
#endregion Fields
#region Properties
public ObservableCollection<MarriedCoupleRow> Rows
{
get
{
return _rows;
}
}
#endregion Properties
#region Commands
#endregion Commands
#region Private Methods
#endregion Private Methods
}
public class MarriedCoupleRow : ViewModelBase
{
private HumanData _Male = null;
public HumanData Male
{
get { return _Male; }
set
{
if (value != _Male)
{
_Male = value;
OnPropertyChanged("Male");
}
}
}
public HumanData Female { get; set; }
}
public class HumanData : INotifyPropertyChanged
{
public string Height { get; set; }
public uint Weight { get; set; }
private string _friendlyName;
public string Information
{
get
{
return _friendlyName;
}
set
{
if (_friendlyName != value)
{
_friendlyName = value;
OnPropertyChanged("Information");
}
}
}
}
And here's the XAML:
<Window x:Class="XMLDemo.Views.XMLDemoView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="XMLDemoView" Height="600" Width="1152">
<DockPanel>
<StackPanel Orientation="Horizontal" Height="120" DockPanel.Dock="Bottom">
<StackPanel Orientation="Horizontal">
<GroupBox Width="249" BorderThickness="2" Height="90">
<GroupBox.Header>
<TextBlock FontSize="12" FontWeight="Bold">Control</TextBlock>
</GroupBox.Header>
<Grid Height="64" Width="223">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.5*" />
<ColumnDefinition Width="0.5*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="0.5*"/>
<RowDefinition Height="0.5*"/>
</Grid.RowDefinitions>
</Grid>
</GroupBox>
</StackPanel>
</StackPanel>
<DataGrid ItemsSource="{Binding Path=Rows, UpdateSourceTrigger=PropertyChanged}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.HeaderTemplate >
<DataTemplate>
<Grid ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Male" />
</Grid>
</DataTemplate>
</DataGridTemplateColumn.HeaderTemplate>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox Text="{Binding Male.Height}" IsEnabled="False" Grid.Row="0"></TextBox>
<TextBox Text="{Binding Male.Weight}" IsEnabled="False" Grid.Row="1"></TextBox>
<TextBox Text="{Binding Male.Information, Mode=TwoWay}" Grid.Row="2"></TextBox>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn>
<DataGridTemplateColumn.HeaderTemplate >
<DataTemplate>
<Grid ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Female" />
</Grid>
</DataTemplate>
</DataGridTemplateColumn.HeaderTemplate>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid >
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox Text="{Binding Female.Height}" IsEnabled="False" Grid.Row="0"></TextBox>
<TextBox Text="{Binding Female.Weight}" IsEnabled="False" Grid.Row="1"></TextBox>
<TextBox Text="{Binding Female.Information}" Grid.Row="2"></TextBox>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
Your TextBox.Text bindings need to be set to updatesource on propertychanged.
<TextBox Text="{Binding Male.Information, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Row="2"></TextBox>
There must be something goofy going on with LostFocus which is the default. I tested this using your code.
I Like your post. As I seen from the coding you are using Complex property binding with DataGrid. Also complex property changes won't be reflect in DataGrid.
However you can achieve this requirement in sample level. I will try to make the sample and let you know.
Regards,
Riyaj Ahamed I

Binding does not work for TemplateSelector

I have one part of the view is dynamic based on a TemplateSelector. However, the binding does not work for the controls in the DataTemplate.(The controls do show on screen, just the conetent/texts are empty). I suspect it's a DataContext issue, but couldn't figure out after a lot of searching on line. Here is my XAML:
<Grid>
<Grid.DataContext>
<local:MyViewModel/>
</Grid.DataContext>
<Grid.Resources>
<DataTemplate x:Key="T1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Grid.Row="0"
Text="Music"
Style="{StaticResource ResourceKey=textBlockStyle}" />
<TextBox Grid.Column="1"
Grid.Row="0"
Style="{StaticResource ResourceKey=textBoxStyle}"
Text="{Binding Path=MusicName}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="T2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Grid.Row="0"
Text="Currency"
Style="{StaticResource ResourceKey=textBlockStyle}" />
<ComboBox Grid.Column="1"
Grid.Row="0"
Style="{StaticResource ResourceKey=comboBoxStyle}"
ItemsSource="{Binding Path=Currency_List}"
SelectedItem="{Binding Path=Currency}" />
</Grid>
</DataTemplate>
<local:ProductTypeTemplateSelector T1="{StaticResource ResourceKey=T1}"
T2="{StaticResource ResourceKey=T2}"
x:Key="myTemplateSelector" />
<Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="*" />
<RowDefinition Height="40"/>
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<!-- This biding works -->
<TextBlock Grid.Row="0"
Text="{Binding Path=MusicName}"/>
<!-- This biding does not work -->
<ContentControl Grid.Row="1"
Name="ccc"
Content="{Binding Path=Product_Type}"
ContentTemplateSelector="{StaticResource myTemplateSelector}">
</ContentControl>
</Grid>
This is my View Model (Technically, it is View Model and Model mixed together. I am not really implementing a full MVVM pattern)
public class MyViewModel: INotifyPropertyChanged
{
public MyViewModel()
{
SetLists();
}
protected void SetLists()
{
SetList_Product_Type();
SetList_Currency();
}
protected void SearchAndPopulate()
{
string query = string.Format("select * from dbo.vehicle_attributes where ticker like '%{0}%'", Search_Text);
DataView dv = DAL.ExecuteQuery(query);
if (dv.Count > 0)
{
DataRowView dvr = dv[0];
Vehicle_Id = int.Parse(dvr["vehicle_id"].ToString());
Product_Type = dvr["product_type_name"].ToString();
Vehicle_Name = dvr["vehicle_name"].ToString();
Is_Onshore = dvr["domicile_name"].ToString() == "Onshore";
Currency = dvr["currency"].ToString();
CUSIP = dvr["CUSIP"].ToString();
ISIN = dvr["isin"].ToString();
Ticker = dvr["ticker"].ToString();
Valoren = dvr["valoren"].ToString();
PC_Class = PC_Class_List.Find(x => x.Class_Name == dvr["class_name"].ToString());
Implementation_Type = Implementation_Type_List.Find ( x => x.Implementation_Type_Name == dvr["implementation_type_name"].ToString());
Price_Frequency = Price_Frequency_List.Find( x => x.Price_Frequency_Name == dvr["price_freq_name"].ToString());
Status = Status_List.Find( x => x.Status_Name == dvr["status_name"].ToString());
if (!string.IsNullOrEmpty(dvr["last_status_update"].ToString()))
{
Status_Date = DateTime.Parse(dvr["last_status_update"].ToString());
}
else
{
Status_Date = DateTime.MinValue;
}
switch (Product_Type)
{
case "Mutual Fund":
query = string.Format("select lf.dividend_currency, i.ticker from dbo.liquid_funds lf " +
"left join dbo.vehicles i on i.vehicle_id = lf.index_id " +
"where lf.vehicle_id ='{0}'",Vehicle_Id);
DataView dv_mutual_fund = DAL.ExecuteQuery(query);
if(dv_mutual_fund.Count > 0)
{
DataRowView dvr_mutual_fund = dv_mutual_fund[0];
Dividend_Currency = dvr_mutual_fund["dividend_currency"].ToString();
Benchmark_Ticker = dvr_mutual_fund["ticker"].ToString();
}
break;
default:
break;
}
}
}
public ICommand SearchVehicleCommand
{
get
{
return new Command.DelegateCommand(SearchAndPopulate);
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
//ProductType
protected List<string> _product_Type_List = new List<string>();
public List<string> Product_Type_List
{
get
{
return _product_Type_List;
}
set
{
_product_Type_List = value;
OnPropertyChanged("Product_Type_List");
}
}
protected void SetList_Product_Type()
{
string query = "SELECT * FROM dbo.product_types WHERE is_enabled = 1";
DataView dv = DAL.ExecuteQuery(query);
List<string> l = new List<string>();
for (int i = 0; i < dv.Count; i++)
{
l.Add(dv[i]["product_type_name"].ToString());
}
Product_Type_List = l;
}
protected string _product_type;
public string Product_Type
{
get
{
return _product_type;
}
set
{
_product_type = value;
OnPropertyChanged("Product_Type");
SetList_Implementation_Type();
}
}
//Currency
protected List<string> _currency_List = new List<string>();
public List<string> Currency_List
{
get
{
return _currency_List;
}
set
{
_currency_List = value;
OnPropertyChanged("Currency_List");
}
}
protected void SetList_Currency()
{
string query = "SELECT currency FROM dbo.currencies";
DataView dv = DAL.ExecuteQuery(query);
List<string> l = new List<string>();
for (int i = 0; i < dv.Count; i++)
{
l.Add(dv[i]["currency"].ToString());
}
Currency_List = l;
}
protected string _currency;
public string Currency
{
get
{
return _currency;
}
set
{
_currency = value;
OnPropertyChanged("Currency");
}
}
// Music Name
protected string _musicName;
public string MusicName
{
get
{
return _musicName;
}
set
{
_musicName = value;
OnPropertyChanged("MusicName");
}
}
}
This is the class interface (sorry for the formatting above, but somehow I can't get it right):
And this is my DelegateCommand class:
public class DelegateCommand : ICommand
{
private readonly Action _action;
public DelegateCommand(Action action)
{
_action = action;
}
public void Execute(object parameter)
{
_action();
}
public bool CanExecute(object parameter)
{
return true;
}
}
This is the DataTemplateSelector:
public class MyTemplateSelector : DataTemplateSelector
{
public DataTemplate T1 { get; set; }
public DataTemplate T2 { get; set; }
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
string product_type = (string)item;
if (product_type == "Type1")
return T1;
else
return T2;
}
}
The DataContext of a DataTemplate is set to the object that it is bound to. So in your case the DataContext for your Templates are Product_Type and you are expecting it to be MyViewModel.
There is a workaround for what you need. It uses a RelativeSource binding and FindAncester to access the DataContext of the Parent object.
Your DataTemplate bindings should look like this:
XAML
<DataTemplate x:Key="T1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Grid.Row="0"
Text="Music"
Style="{StaticResource ResourceKey=textBlockStyle}" />
<TextBox Grid.Column="1"
Grid.Row="0"
Style="{StaticResource ResourceKey=textBoxStyle}"
Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContentControl}}, Path=DataContext.MusicName}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="T2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Grid.Row="0"
Text="Currency"
Style="{StaticResource ResourceKey=textBlockStyle}" />
<ComboBox Grid.Column="1"
Grid.Row="0"
Style="{StaticResource ResourceKey=comboBoxStyle}"
ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContentControl}}, Path=DataContext.Currency_List}"
SelectedItem="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContentControl}}, Path=DataContext.Currency}" />
</Grid>
</DataTemplate>
From MSDN
Find Ancestor - Refers to the ancestor in the parent chain of the data-bound element. You can use this to bind to an ancestor of a specific type or its subclasses.
The AncestorType attribute goes up the visual tree and finds the first Ancestor of the given type, in this case it's looking for ContentControl the path to the required property can then be set relative to this.
Update
Think of a template as a guide on how to display an object. The DataContext of the DataTemplate is going to be whatever object it is asked it to display.
In this case the ContentControl is told to display Product_Type and, depending on the value of Product_Type, to use a particular Template. Product_Type is given to the DataTemplate and becomes the DataContext.
WPFTutorials has some good examples.

Resources