Allowing named elements in a multi-content custom control - c#

Requirement
Let's start with what I am trying to achieve. I want to have a grid with 2 columns and a grid splitter (there is a little more to it that that, but let's keep it simple). I want to be able to use this grid in a lot of different places, so instead of creating it each time I want to make a custom control that contain two ContentPresenters.
The end goal is effectively to be able to write XAML like this:
<MyControls:MyGrid>
<MyControls:MyGrid.Left>
<Label x:Name="MyLabel">Something unimportant</Label>
</MyControls:MyGrid.Left>
<MyControls:MyGrid.Right>
<Label>Whatever</Label>
</MyControls:MyGrid.Right>
</MyControls:MyGrid>
IMPORTANT: Notice that I want to apply a Name to my Label element.
Attempt 1
I did a lot of searching for solutions, and the best way I found was to create a UserControl along with a XAML file that defined my grid. This XAML file contained the 2 ContentPresenter elements, and with the magic of binding I was able to get something working which was great. However, the problem with that approach is not being able to Name the nested controls, which results in the following build error:
Cannot set Name attribute value 'MyName' on element 'MyGrid'. 'MyGrid'
is under the scope of element 'MyControls', which already had a name
registered when it was defined in another scope.
With that error in hand, I went back to Dr. Google...
Attempt 2 (current)
After a lot more searching I found some information here on SO that suggested the problem was due to having an associated XAML file with the MyGrid class, and the problem should be solvable by removing the XAML and creating all the controls via code in the OnInitialized method.
So I headed off down that path and got it all coded and compiling. The good news is that I can now add a Name to my nested Label control, the bad news is nothing renders! Not in design mode, and not when running the application. No errors are thrown either.
So, my question is: What am I missing? What am I doing wrong?
I am also open to suggestions for other ways to meet my requirements.
Current code
public class MyGrid : UserControl
{
public static readonly DependencyProperty LeftProperty = DependencyProperty.Register("Left", typeof(object), typeof(MyGrid), new PropertyMetadata(null));
public object Left
{
get { return (object)GetValue(LeftProperty); }
set { SetValue(LeftProperty, value); }
}
public static readonly DependencyProperty RightProperty = DependencyProperty.Register("Right", typeof(object), typeof(MyGrid), new PropertyMetadata(null));
public object Right
{
get { return (object)GetValue(RightProperty); }
set { SetValue(RightProperty, value); }
}
Grid MainGrid;
static MyGrid()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyGrid), new FrameworkPropertyMetadata(typeof(MyGrid)));
}
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
//Create control elements
MainGrid = new Grid();
//add column definitions
ColumnDefinition leftColumn = new ColumnDefinition()
{
Name = "LeftColumn",
Width = new GridLength(300)
};
MainGrid.ColumnDefinitions.Add(leftColumn);
MainGrid.ColumnDefinitions.Add(new ColumnDefinition()
{
Width = GridLength.Auto
});
//add grids and splitter
Grid leftGrid = new Grid();
Grid.SetColumn(leftGrid, 0);
MainGrid.Children.Add(leftGrid);
GridSplitter splitter = new GridSplitter()
{
Name = "Splitter",
Width = 5,
BorderBrush = new SolidColorBrush(Color.FromArgb(255, 170, 170, 170)),
BorderThickness = new Thickness(1, 0, 1, 0)
};
MainGrid.Children.Add(splitter);
Grid rightGrid = new Grid();
Grid.SetColumn(rightGrid, 1);
MainGrid.Children.Add(rightGrid);
//add content presenters
ContentPresenter leftContent = new ContentPresenter();
leftContent.SetBinding(ContentPresenter.ContentProperty, new Binding("Left") { Source = this });
leftGrid.Children.Add(leftContent);
ContentPresenter rightContent = new ContentPresenter();
rightContent.SetBinding(ContentPresenter.ContentProperty, new Binding("Right") { Source = this });
rightGrid.Children.Add(rightContent);
//Set this content of this user control
this.Content = MainGrid;
}
}

After some discussion via comments, it quickly became clear that neither of my attempted solutions was the correct way to go about it. So I set out on a third adventure hoping this one would be the final solution... and it seems it is!
Disclaimer: I do not yet have enough experience with WPF to confidently say that my solution is the best and/or recommended way to do this, only that it definitely works.
First of all create a new custom control: "Add" > "New Item" > "Custom Control (WPF)". This will create a new class that inherits from Control.
In here we put our dependency properties for bind to out content presenters:
public class MyGrid : Control
{
public static readonly DependencyProperty LeftProperty = DependencyProperty.Register("Left", typeof(object), typeof(MyGrid), new PropertyMetadata(null));
public object Left
{
get { return (object)GetValue(LeftProperty); }
set { SetValue(LeftProperty, value); }
}
public static readonly DependencyProperty RightProperty = DependencyProperty.Register("Right", typeof(object), typeof(MyGrid), new PropertyMetadata(null));
public object Right
{
get { return (object)GetValue(RightProperty); }
set { SetValue(RightProperty, value); }
}
static MyGrid()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyGrid), new FrameworkPropertyMetadata(typeof(MyGrid)));
}
}
When you add this class file in Visual Studio, it will automatically create a new "Generic.xaml" file in the project containing a Style for this control, along with a Control Template within that style - this is where we define our control elements...
<Style TargetType="{x:Type MyControls:MyGrid}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type MyControls:MyGrid}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="500" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<ContentPresenter x:Name="LeftContent" />
</Grid>
<GridSplitter Width="5" BorderBrush="#FFAAAAAA" BorderThickness="1,0,1,0">
</GridSplitter>
<Grid Grid.Column="1">
<ContentPresenter x:Name="RightContent" />
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The final step is to hook up the bindings for the 2 content presenters, so back to the class file.
Add the following override method to the MyGrid class:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//Apply bindings and events
ContentPresenter leftContent = GetTemplateChild("LeftContent") as ContentPresenter;
leftContent.SetBinding(ContentPresenter.ContentProperty, new Binding("Left") { Source = this });
ContentPresenter rightContent = GetTemplateChild("RightContent") as ContentPresenter;
rightContent.SetBinding(ContentPresenter.ContentProperty, new Binding("Right") { Source = this });
}
And that's it! The control can now be used in other XAML code like so:
<MyControls:MyGrid>
<MyControls:MyGrid.Left>
<Label x:Name="MyLabel">Something unimportant</Label>
</MyControls:MyGrid.Left>
<MyControls:MyGrid.Right>
<Label>Whatever</Label>
</MyControls:MyGrid.Right>
</MyControls:MyGrid>
Thanks to #NovitchiS for your input, your suggestions were vital in getting this approach to work

Related

WPF Add trigger to an existing style from code behind

I have a TabControl with some tabs declared in XAML. I want to add new tabs and bind their IsEnabled properties to some properties of their content:
for (int i = 0; i < context.Pictures.Count; ++i)
{
var tabItem = new TabItem();
var title = "Some title"
tabItem.Header = title;
var image = new Image();
Binding sourceBinding = new Binding(nameof(context.Pictures) + $"[{i}]");
sourceBinding.Source = context;
image.SetBinding(Image.SourceProperty, sourceBinding);
image.Width = 800;
image.Height = 600;
DataTrigger isEnabledTrigger = new DataTrigger() { Binding = sourceBinding, Value = null };
isEnabledTrigger.Setters.Add(new Setter(TabItem.IsEnabledProperty, false));
tabItem.Content = image;
tabControl.Items.Add(tabItem);
}
I want to disable tab if the picture inside is null (apply isEnabledTrigger). Problem here is that style of tabItem is derived from tabControl containing it, so I cannot just create a style with my trigger and apply it to TabItem. Sure, I could just copy original style and hardcode it, but I don't think it's a good way to solve my problem.
So, to solve my problem I have two ideas:
Create a shallow copy of existing style, add trigger and apply it
Load original style from XAML, add trigger and apply it (may be difficult, since it lies in another project)
Is there more rational way to bind TabControls IsEnabled to contained Images value?
Don't add TabItem directly. Use data models. This is recommended approach for all item controls. Then define a DataTemplate for the data model(s) and assign it to TabControl.ContentTemplate.
Use the TabControl.ItemTemplate to layout the header.
Defining a Style for the TabControl.ItemContainerStyle allows you to set up the required triggers quite easily. Doing layout using C# is never a good idea. Always use XAML.
See: Data binding overview in WPF, Data Templating Overview
The minimal model class should look like this:
PictureModel.cs
// All binding source models must implement INotifyPropertyChanged
public class PictureModel : INotifyPropertyChanged
{
private string title;
public string Title
{
get => this.title;
set
{
this.title = value;
OnPropertyChanged();
}
}
private string source;
public string Source
{
get => this.source;
set
{
this.source = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
ViewModel.cs
class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<PictureModel> Pictures { get; }
private void CreateTabItems(Context context)
{
foreach (string imageSource in context.Pictures)
{
var pictureModel = new PictureModel()
{
Title = "Some Title",
Source = imageSource
};
this.Pictures.Add(pictureModel);
}
}
}
MainWindow.xaml
<Window>
<Window.DataContext>
<ViewModel />
</Window.DataContext>
<!-- Layout the tab content -->
<TabControl ItemsSource="{Binding Pictures}">
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type viewModels:PictureModel}">
<Image Source="{Binding Source}" />
</DataTemplate>
</TabControl.ContentTemplate>
<!-- Layout the tab header -->
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModels:PictureModel}">
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</TabControl.ItemTemplate>
<!-- Setup triggers. The DataContext is the current data model -->
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Source}" Value="{x:Null}">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
</Window>
You should base your Style on the current Style:
Style style = new Style(typeof(TabItem))
{
BasedOn = FindResource(typeof(TabItem)) as Style
};
DataTrigger isEnabledTrigger = new DataTrigger() { Binding = sourceBinding, Value = null };
isEnabledTrigger.Setters.Add(new Setter(TabItem.IsEnabledProperty, false));
style.Triggers.Add(isEnabledTrigger);
tabItem.Style = style;
Or
Style style = new Style(typeof(TabItem))
{
BasedOn = tabControl.ItemContainerStyle
};
...
...depending on how your current Style is applied.
This is how you would extend an existing Style with your DataTrigger and this is a good way of solving this.

Question about using the WindowsFormsHost. (with DataBinding)

My English skill is poor because I'm not a native English speaker.
I hope you to understand.
I need to use the WindowsFormsHost control because using the DataGridView of the WinForm.
I can't DataGrid control in WPF because of some reasons.
In the .cs file, I succeeded using the WindowsFormsHost with the DataGridView of the WinForm.
The code is as shown below.
var tabItem = new ClosableTab();
#region Core Logic
var winformControl = new WindowsFormsHost();
winformControl.VerticalAlignment = VerticalAlignment.Stretch;
winformControl.HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch;
winformControl.Child = new DataGridView();
DataGridView parsingTableView = winformControl.Child as DataGridView;
parsingTableView.EditMode = DataGridViewEditMode.EditProgrammatically;
parsingTableView.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
parsingTableView.DataSource = mainWindow.editor.TokenTable;
parsingTableView.CellMouseEnter += new DataGridViewCellEventHandler(this.tableGridView_CellMouseEnter);
tabItem.Content = winformControl;
#endregion
this.mainWindow.tablControl.Items.Add(tabItem);
Now I want to convert the above logic to the XAML so I wrote the logic as shown below.
First, I defined DataTemplate to display the DataTable type.
// In resource file
xmlns:systemData="clr-namespace:System.Data;assembly=System.Data"
xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
<DataTemplate DataType="{x:Type systemData:DataTable}">
<WindowsFormsHost>
<WindowsFormsHost.Child>
<wf:DataGridView DataSource="{Binding}" EditMode="EditProgrammatically" AutoSizeColumnsMode="AllCells"/>
</WindowsFormsHost.Child>
</WindowsFormsHost>
</DataTemplate>
Now the above DataTemplate is used when binding to Content as shown the below code.
// In xaml file
<Grid>
<TabControl ItemsSource="{Binding SelectedItem}">
<TabItem Header="{lex:Loc Key=TokenTable}" Content="{Binding TokenTable}"/>
</TabControl>
</Grid>
When I execute the above code, I faced an error as below at the DataSource="{Binding}".
If I have to translate the above error using my poor English skill.
The error tells me. "It can't configure 'Binding' on the 'DataSource' property."
"The 'Binding' can configure only on the DependencyProperty of the DependencyObject."
I think I know what error trying to say to me but I don't know what I do to solve the above problem.
What should I do to solve this problem?
Thank you for reading.
You can't bind directly to the DataSource property of a DataGridView because it's not a dependency property.
What you can do is to work around this by create an attached property that you set on the WindowsFormsHost as suggested here, and then set the property of the DataGridView using the callback of the attached property:
public static class WindowsFormsHostMap
{
public static readonly DependencyProperty DataSourceProperty
= DependencyProperty.RegisterAttached("DataSource", typeof(object),
typeof(WindowsFormsHostMap), new PropertyMetadata(OnPropertyChanged));
public static string GetText(WindowsFormsHost element) => (string)element.GetValue(DataSourceProperty);
public static void SetText(WindowsFormsHost element, object value) => element.SetValue(DataSourceProperty, value);
static void OnPropertyChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var dataGridView = (sender as WindowsFormsHost).Child as DataGridView;
dataGridView.DataSource = e.NewValue;
}
}
XAML:
<WindowsFormsHost local:WindowsFormsHostMap.DataSource="{Binding}">

Programmatically created controls not rendering

As I was required to sort of mask input in a textbox, I decided to construct my own control to handle this.
One of many templates could be "Size {enter size} Colour {enter colour}" which I've broken down to create a series of controls. The custom control that extends StackPanel which I've named CustomTextBox generates the following from the constructor.
// Pseudo
Children = {
Label = { Content = "Size" },
TextBox = { Text = "enter size" },
Label = { Content = "Colour" },
TextBox = { Text = "enter colour" }
// .. and an arbitrary amount of more Labels and TextBoxes in no particular order
}
So far so good. But when I want it to render.. That's where my headache starts.
I've tried to add the controls to the Children property and Measure/Arrange on the parent, itself and all the Children. ActualHeight and ActualWidth do change to something other than 0, but they won't render/display/become visible whatsoever.
I've also tried to use an ItemsControl and add the controls to the ItemsSource property to no avail.
I've tried to predefine sizes on everything, colour the background red and all, but the elusive controls remain to be caught and tied to my screen.
There's got to be a huge "Oooh..." here that I just can't find. I refuse to believe that this can't be done. I mean, it's WPF. WPF is awesome.
Edit Updated to what I currently have that seems most likely to work - still doesn't though.
Whatever I do in the designer shows up, but nothing I do in the CustomTextBox makes any visible difference.
Edit
New headline that fits the problem better.
Also, I've found several examples of programmatically adding controls. Take this article for example. I fail to see the difference between my scenario and theirs, except that theirs work and the buttons are visible.
Update3
The mistake was to assume, that one can simply replace control in visual tree by assigning in codebehind a new control to it's name (specified in xaml)
Updated2
Your mistake was following. If you write
<TextBlock Name="tb" Text="tb"/>
and then in code you will do
tb = new TextBlock() { Text = "Test" };
then you will have a new textblock as a variable, and nothing in xaml will change. You either have to change existing control, or remove old control and add new.
I'm talking about your Headline, Subtext & Description. You don't change them
Updated:
Here is an example of dynamically creating controls by specifying input mask:
MainWindow.xaml
<Window x:Class="WpfApplication35.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" xmlns:local="clr-namespace:WpfApplication35">
<Grid>
<local:UserControl1 x:Name="myUserControl"/>
</Grid>
</Window>
MainWindow.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
myUserControl.BuildControls("a {enter a} b {enter b1}{enter c2}");
}
}
UserControl1.xaml
<UserControl x:Class="WpfApplication35.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="30" d:DesignWidth="300">
<WrapPanel Name="root" Orientation="Horizontal"/>
</UserControl>
UserControl1.cs
public partial class UserControl1 : UserControl
{
public List<CustomField> Fields = new List<CustomField>();
public UserControl1()
{
InitializeComponent();
}
public UserControl1(string mask)
{
InitializeComponent();
BuildControls(mask);
}
public void BuildControls(string mask)
{
//Parsing Input
var fields = Regex.Split(mask, #"(.*?\}\s)");
foreach (var item in fields)
{
if (item != "")
{
int index = item.IndexOf('{');
string namestring = item.Substring(0, index).Trim();
var field = new CustomField() { Name = namestring };
string valuesstring = item.Substring(index, item.Length - index).Trim();
var values = valuesstring.Split(new char[] { '{', '}' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var val in values)
{
var valuewrapper = new FieldValue() { Value = val };
field.Values.Add(valuewrapper);
}
Fields.Add(field);
}
}
foreach (var field in Fields)
{
var stackPanel = new StackPanel() { Orientation = Orientation.Horizontal };
var label = new Label() { Content = field.Name, Margin = new Thickness(4) };
stackPanel.Children.Add(label);
foreach (var item in field.Values)
{
var tb = new TextBox() { Margin = new Thickness(4), Width = 200 };
tb.SetBinding(TextBox.TextProperty, new Binding() { Path = new PropertyPath("Value"), Source = item, Mode = BindingMode.TwoWay });
stackPanel.Children.Add(tb);
}
root.Children.Add(stackPanel);
}
}
}
public class CustomField
{
public string Name { get; set; }
public List<FieldValue> Values = new List<FieldValue>();
}
public class FieldValue
{
public string Value { get; set; }
}
This way fields and values are gonna be represented by Fields collection in UserControl1. Values of fields are updated as user types something. But only one-way, i.e. user input updates corresponding Value property, but changing Value property at runtime will not affect corresponding textbox. To implement updating from Value to textbox you have to implement INotifyProperty interface
Obsolete
Since you've asked.
There are hundreds of possible implementations, depending on what are you trying to archieve, how do you want validation to be, do you want to use MVVM, do you want to use bindings etc. There are generally 2 approaches : creating usercontrol and creating custom control. First one suits you better I believe.
Create a usercontrol with following xaml:
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Content="Size: " Grid.Column="0"/>
<TextBox Name="tbSize" Grid.Column="1"/>
<Label Content="Colour:" Grid.Column="2"/>
<TextBox Name="tbColour" Grid.Column="3"/>
</Grid>
In code-behind you can access TextBoxes by their name and do whatever you want to do.
You can use usercontrol in both xaml and codebehind.
In xaml:
Specify alias for namespace of your usercontrol (look at xmlns:local)
<Window x:Class="WpfApplication35.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"
xmlns:local="clr-namespace:WpfApplication35">
<Grid>
<local:UserControl1/>
</Grid>
</Window>
In codebehind you can use it like this:
public MainWindow()
{
InitializeComponent();
var myUserControl = new UserControl1();
}
There is a lot to say and these are basic things, so check tutorials and ask questions.
P.S. If you are learning WPF it's mandatory to learn bindings.

WP7.8: Bound items in scrollbox updated with wrong data

Overview
I have an application, that displays data from an observable collection. The observable collection is (in this debugging setting) created and instanciated only once, then the values stay the same.
The main view of the application contains a ListBox that is bound to said observable collection:
<ListBox x:Name="MainListBox" ItemsSource="{Binding Items}" SelectionChanged="MainListBox_SelectionChanged" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel MinWidth="456" MaxWidth="456" Background="White" Margin="0,0,0,17">
<sparklrControls:SparklrText Post="{Binding Path=.}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<!-- Workaround used to stretch the child elements to the full width -> HorizontalContentAlignment won't work for some reason... -->
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
The child items are bound to a UserControl. This UserControl implements a DependancyProperty which the child elements are bound to:
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(object), new PropertyMetadata(textPropertyChanged));
private static void postPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SparklrText control = d as SparklrText;
control.Post = (ItemViewModel)e.NewValue;
}
Binding to the post property configures other variables via the getter of the Post property
public ItemViewModel Post
{
get
{
return post;
}
set
{
if (post != value)
{
this.ImageLocation = value.ImageUrl;
this.Username = value.From;
this.Comments = value.CommentCount;
this.Likes = value.LikesCount;
this.Text = value.Message;
post = value;
}
}
}
This setter configures other which in turn set up elements in the user control. Nothing in the user control is bound, the few updates are done with direct access to the respective Content/Text properties. ImageLocation performs an asynchronous download of an image with
private void loadImage(string value)
{
WebClient wc = new WebClient();
wc.OpenReadCompleted += (sender, e) =>
{
image = new BitmapImage();
image.SetSource(e.Result);
MessageImage.Source = image;
};
wc.OpenReadAsync(new Uri(value));
}
Issue
When I scroll down in the list box and back up, the setter of Post is executed when the owning element comes back into view. The problem: value is a different instance of ItemViewModel. The ListBox ItemsSource is not accessed in any way from outside the class. When scrolling back up, it seems like the wrong Items are bound to the elements, resulting in distorted designs. Are there any issues with the Binding that cause this?
The issue was caused by the ListBox. Elements that are scroll out of view are recycled and appended on the other side. In the code above, a asynchronous operation did not check if the result was still valid, causing wrong display data.

Resizing Issues with DataGrid containing wrapping TextBlocks

I have a really disturbing issue with .NET 4.0 DataGrid. I have proportional template columns containing TextBlock with textWrapping enabled.
Problem is, the height of the DataGrid is not correct at load time (it's sized as if the textblock were all wrapped at their maximum.) and does not update their size when resizing. It appears to be a layout issue (MeasureOverride and ArrangeOverride seems to be called when proportional sizes are not resolved, and not called afterwards...) but I haven't been able to solve it.
Here is a simplified code that shows the issue :
MainWindow.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="700" Width="525">
<StackPanel Width="500">
<Button Content="Add DataGrid" Click="Button_Click" />
<ItemsControl x:Name="itemsControl">
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Control.Margin" Value="5" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</StackPanel>
MainWindow.xaml.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace WpfApplication1
{
public partial class MainWindow : System.Windows.Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
itemsControl.Items.Add(CreateDataGrid());
}
private DataGrid CreateDataGrid()
{
var dataGrid = new DataGrid() { HeadersVisibility = DataGridHeadersVisibility.Column };
dataGrid.MaxWidth = 500;
dataGrid.Background = Brushes.LightSteelBlue;
dataGrid.Columns.Add(GetDataGridTemplateColumn("Label", "Auto"));
dataGrid.Columns.Add(GetDataGridTemplateColumn("Value", "*"));
dataGrid.Items.Add(new Entry() { Label = "Text Example 1", Value = "Some wrapped text" });
dataGrid.Items.Add(new Entry() { Label = "Text Example 2", Value = "Some wrapped text" });
return dataGrid;
}
private DataGridTemplateColumn GetDataGridTemplateColumn(string bindingPath, string columnWidth)
{
DataGridTemplateColumn result = new DataGridTemplateColumn() { Width = (DataGridLength)(converter.ConvertFrom(columnWidth))};
FrameworkElementFactory cellTemplateframeworkElementFactory = new FrameworkElementFactory(typeof(TextBox));
cellTemplateframeworkElementFactory.SetValue(TextBox.NameProperty, "cellContentControl");
cellTemplateframeworkElementFactory.SetValue(TextBox.TextProperty, new Binding(bindingPath));
cellTemplateframeworkElementFactory.SetValue(TextBox.TextWrappingProperty, TextWrapping.Wrap);
result.CellTemplate = new DataTemplate() { VisualTree = cellTemplateframeworkElementFactory };
return result;
}
private static DataGridLengthConverter converter = new DataGridLengthConverter();
}
public class Entry
{
public string Label { get; set; }
public string Value { get; set; }
}
}
I finally came up with a rather dirty solution, but at least it works : I'm manually updating the Height of the grid by computing myself the right value, taking into account Rows height, Grid Padding, Grid border and ColumnHeaderRow height. I need to update it on OnPropertyChanged (for padding and grid border), DataGridColumn.SizeChanged, DataGridColumn.RowUnloaded, ColumnHeaderPresenter.SizeChanged, and a few others.
Only issue is that it works correctly with the default DataGrid ControlTemplate but would not be correct anymore if a template was to change the grid render.
Finally found a solution : setting CanContentScroll to false on the DataGrid fixed the issue.
<Setter Property="ScrollViewer.CanContentScroll" Value="False" />

Categories