I am customizing WPF DataGrid in a generic way. consider that I have defined an enum of Creatures like this:
public enum CreatureType
{
Human,
Animal,
Insect,
Virus
}
then I have an attached property of the above type so it can be attached to any control I want, that called CreatureTypeProeprty. then I want to attach this property to DataGridTemplateColumn and I want to access this inside the Template. for example consider the following reproducible code of my problem:
<DataGridTemplateColumn local:CreatureTypeProeprty.Value = "Human" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Path=(local:CreatureTypeProeprty.Value), RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGridTemplateColumn }}"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
This doesn't work and I know why because DataGrid column is not part of the visual tree. but I can't find any way to solve this issue. I also tried This but this solution can not be used in a generic way and ends up in a very dirty solution! I'm looking for a more sophisticated and clean way. I also tried using the name for the column and specifying ElementName it works but also this can not be used when there is multiple columns and I have to repeat codes every time.
CodePudding user response:
The datagrid column is more a conceptual thing. It's not so much not in the visual tree as not there at all.
There is no spoon.
One way to do this sort of thing would be to use a list and index it. In your window viewmode
public list<CreatureType> Creatures {get;set;}
Obviously, load that up with CreatureTypes in the same order as your columns.
You can then reference using something like
Text="{Binding DataContext.Creatures[4]}
, RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=Datagrid}}
Unfortunately, you need a fixed number for indexing.
You could perhaps dynamically build your columns and add them to the datagrid.
I would do that by defining a string template and substituting values, then using xamlreader.parse() to build a datagrid column which you add to your datagrid.
In my sample I have a col.txt file. This is a sort of template for my datagrid columns.
It is pretty much a piece of datagrid column markup.
<?xml version="1.0" encoding="utf-8" ?>
<DataGridTemplateColumn>
<DataGridTemplateColumn.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="xxMMMxx" TextAlignment="Center" Grid.ColumnSpan="2"/>
<TextBlock Text="Units" Margin="2,0,2,0" Grid.Row="1"/>
<TextBlock Text="Value" Margin="2,0,2,0" Grid.Row="1" Grid.Column="2" />
</Grid>
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding MonthTotals[xxNumxx].Products}" Margin="2,0,2,0" TextAlignment="Right"/>
<TextBlock Text="{Binding MonthTotals[xxNumxx].Total}" Margin="2,0,2,0" TextAlignment="Right" Grid.Column="1"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
It's showing totals for months so obviously 12 months and my array here is monthtotals[12]. Obviously, this is a different thing and illustrating the approach.
I also have a dg.txt file which is of course something rather similar for my datagrid. But that'd be repetitive.
And I have some code substitutes strings, parses the result and appends a built column:
private void Button_Click(object sender, RoutedEventArgs e)
{
// Get the datagrid shell
XElement xdg = GetXElement(@"pack://application:,,,/dg.txt");
XElement cols = xdg.Descendants().First(); // Column list
// Get the column template
XElement col = GetXElement(@"pack://application:,,,/col.txt");
DateTime mnth = DateTime.Now.AddMonths(-6);
for (int i = 0; i < 6; i )
{
DateTime dat = mnth.AddMonths(i);
XElement el = new XElement(col);
// Month in mmm format in header
var mnthEl = el.Descendants("TextBlock")
.Single(x => x.Attribute("Text").Value.ToString() == "xxMMMxx");
mnthEl.SetAttributeValue("Text", dat.ToString("MMM"));
string monthNo = dat.AddMonths(-1).Month.ToString();
// Month as index for the product
var prodEl = el.Descendants("TextBlock")
.Single(x => x.Attribute("Text").Value == "{Binding MonthTotals[xxNumxx].Products}");
prodEl.SetAttributeValue("Text",
"{Binding MonthTotals[" monthNo "].Products}");
// Month as index for the total
var prodTot = el.Descendants("TextBlock")
.Single(x => x.Attribute("Text").Value == "{Binding MonthTotals[xxNumxx].Total}");
prodTot.SetAttributeValue("Text",
"{Binding MonthTotals[" monthNo "].Total}");
cols.Add(el);
}
string dgString = xdg.ToString();
ParserContext context = new ParserContext();
context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
context.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
DataGrid dg = (DataGrid)XamlReader.Parse(dgString, context);
Root.Children.Add(dg);
}
private XElement GetXElement(string uri)
{
XDocument xmlDoc = new XDocument();
var xmltxt = Application.GetContentStream(new Uri(uri));
string elfull = new StreamReader(xmltxt.Stream).ReadToEnd();
xmlDoc = XDocument.Parse(elfull);
return xmlDoc.Root;
}
Might be more complicated than you need but maybe it'll help someone else down the line.
BTW
I suggest an "unknown" or "none" as the first in any enum. This is standard practice in my experience so you can differentiate between a set and unset value.
Q1 How common is this?
Not very. It solves problems I would describe as "awkward" bindings and can simplify UI logic in templating.
Or a sort of super auto generator for columns.
A similar approach can be very useful for user configurable UI or dynamic UI.
Imagine a dynamic UI where the user is working with xml and xml defines the UI. Variable stats or variable points calculation for a tabletop wargaming points calculator.
Or where the user picks the columns they want and how to display them. That can be built as strings and saved to their roaming profile. Loaded from there using xamlreader.load which gives you your entire datagrid definition.
q2 That's what the sample does.
dg.txt looks like
<?xml version="1.0" encoding="utf-8" ?>
<DataGrid
ItemsSource="{Binding SalesMen}"
AutoGenerateColumns="False"
Grid.Column="1"
CanUserAddRows="False"
IsReadOnly="False"
Name="dg">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding FullName}" Header="Name"/>
</DataGrid.Columns>
</DataGrid>
This line gets Columns
XElement cols = xdg.Descendants().First(); // Column list
And this line append each column
cols.Add(el);
It is so there are Xelement nodes you can find that the file is manipulated as xml rather than just as a string.