WPF - Reuse templates with different bindings - wpf

Why not close this question?
Please don't close the question: the suggested link doesn't contain an answer, because there is no Binding property in DataGridTemplateColumn and I can't find a way to bind data to it.
It seems to be possible to use Text={Binding} inside the data template and this is "half" of the answer
Original question
I'm sort of a newbie in WPF.
I have a DataGrid where I want to have specific templates for some columns, but all the templates are the same.
For instance
<DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<!-- first column is a standard column, ok-->
<DataGridTextColumn Header="First Column" Binding="{Binding FirstColumn}"/>
<!-- a few of the other columns use a custom template-->
<DataGridTemplateColumn Header="Second Column">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding SecondColumn, UpdateSourceTrigger=PropertyChanged}"/
<OtherThings />
<OtherThings />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<More Columns Exactly like the above, with different bindings/>
<!-- Column3 Binding={Binding Column3}-->
<!-- Column4 Binding={Binding Column4}-->
</DataGrid.Columns>
</DataGrid>
How can I create a template as a static resource in a way that I can set the binding of this template?
I tried to create a static resource like
<DataTemplate x:Key="ColumnTemplate">
<TextBox Text={I have no idea what to put here}/>
<OtherThings/>
<OtherThings/>
<DataTemplate>
And use it like
<DataGrid>
<DataGrid.Columns>
<!-- Where does the Header go?, where does the binding go?-->
<!-- binds to column 2-->
<DataGridTemplateColumn CellTemplate="{StaticResource ColumnTemplate}">
<!-- binds to column 3-->
<DataGridTemplateColumn CellTemplate="{StaticResource ColumnTemplate}">
</DataGrid.Columns>
</DataGrid>
But I really don't know how to make the binding correctly from the item to the textbox inside the template.
How can I achieve this?

In principle, you can do this.
Whether this is a good plan or not is arguable though.
You can change the cell template to surround whatever the column binds to with other markup. Which seems to be what you want.
There are some subtelties to this in that the datagrid will switch out content of a cell from a textblock to textbox if you allow editing in the datagrid. Which is inadvisable for anything but trivial requirements - but that's perhaps a different subject.
In the datagrid or a parent resources:
<Window.Resources>
<Style x:Key="StringStyle" TargetType="{x:Type DataGridCell}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<StackPanel>
<ContentControl>
<ContentPresenter/>
</ContentControl>
<TextBlock Text="A constant string"/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
You can then apply that to a column:
<DataGridTextColumn Header="Date" Binding="{Binding DT, StringFormat=\{0:dd:hh:mm:ss.FF\}}"
CellStyle="{StaticResource StringStyle}"
As I mentioned above.
Weird things are likely to happen when you click on the cell to edit it.
You'd probably also want code to focus on the textbox within the contentpresenter.
If you instead wanted to cut down on repeated code you could use xamlreader to build your column dynamically using a template.
If you just have maybe 3 columns are the same then this is likely overkill but it'd be largely copy paste.
I built a sample illustrating this:
https://social.technet.microsoft.com/wiki/contents/articles/28797.wpf-dynamic-xaml.aspx#Awkward_Bindings_Data
https://gallery.technet.microsoft.com/WPF-Dynamic-XAML-Awkward-41b0689f
The sample builds a moving selection of months data.
The relevent code is in mainwindow
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;
}
The col.txt file contains all the "standard" markup for a column. This is then manipulated as xml to replace values.
The result is then turned into wpf ui objects using xamlreader.parse
And again.
Probably way over engineered for a requirement has just a couple of columns.

Related

Getting edited value of DataGridTemplateColumn in DataGrid bound to LINQ query results

I have a WPF app with a datagrid that I am binding to a custom LINQ to SQL query in my ViewModel that brings together columns from two different tables:
public ICollectionView SetsView { get; set; }
public void UpdateSetsView()
{
var sets = (from s in dbContext.Sets
join o in dbContext.SetParts on s.ID equals o.SetID into g1
select new
{
s.ID,
s.SetNumber,
s.SetTitle,
s.SetType,
s.SetNotes,
s.SetUrl,
s.HaveAllParts,
s.NumberOfSets,
s.IsBuilt,
s.DateAdded,
s.LastModified,
UniqueParts = g1.Count(),
TotalParts = g1.Sum(o => o.Quantity)
}
);
SetsView = CollectionViewSource.GetDefaultView(sets);
}
The SetsView collection is bound to my datagrid and since I need to be able to edit the value of SetNotes for any row and save it back to the Sets table in my database I added an event handler for CellEditEnding ( CellEditEnding="dgST_Sets_CellEditEnding") to the DataGrid definition and created this column:
<DataGridTemplateColumn Header="Set Notes"
SortMemberPath="SetNotes"
Width="*">
<DataGridTemplateColumn.HeaderStyle>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
</DataGridTemplateColumn.HeaderStyle>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding SetNotes, Mode=OneWay}" Margin="5,0,5,0"
HorizontalAlignment="Stretch" VerticalAlignment="Center" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding SetNotes, Mode=OneWay}" Margin="5,0,5,0"
HorizontalAlignment="Stretch" VerticalAlignment="Center"
/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
The issue is that when I run the application and edit the Set Notes column in any row I cannot work out how to get the new value of the edited cell from the event args. I thought I could cast the event args EditingElement instance to a TextBox (see handler below) but when I run the app, edit a row and change a value of SetNotes the type of EditingElement is ContentPresenter not TextBox and for the life of me I can't work out how to get the changed value.
private void dgST_Sets_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
string newValue = ((TextBox)e.EditingElement).Text;
//Code here to update table
}
Please remember I am binding to a custom LINQ to SQL query so this is not a classic model binding issue. Also note, when binding the value of SetNotes in my template column I have no option but using Mode=OneWay as using any other option gives me runtime errors about accessing a read only property - could this somehow be the issue?
I've spent hours on this and googled endlessly with no joy - can anyone help me out please?
This should work:
private void dgST_Sets_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
ContentPresenter cp = e.EditingElement as ContentPresenter;
if (cp != null && VisualTreeHelper.GetChildrenCount(cp) > 0)
{
TextBox textBox = VisualTreeHelper.GetChild(cp, 0) as TextBox;
if (textBox != null)
{
string newValue = textBox.Text;
}
}
}

Editable DataGrid - CanUserAddRows="True" not working

I have the following DataGrid:
<DataGrid ItemsSource="{Binding EmployeeList}" CanUserAddRows="True" AutoGenerateColumns="False" Margin="0,0,0,90">
<DataGrid.Columns>
<DataGridTemplateColumn Header="CountryCombo2">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path=DataContext.CountryList, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
DisplayMemberPath="CountryName"
SelectedItem="{Binding EmployeeCountry, Mode=TwoWay}"
SelectedValue="{Binding EmployeeCountry.CountryId}"
SelectedValuePath="CountryId" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
However, I am unable to add new rows to the DataGrid. Please let me know if I need to provide any additional code.
Update :
Screen 1 : This is the screenshot when the window is just loaded with the hardcoded property values. Now I see the empty new row.
Screen 2 : Here I have added data into the new row with values Rambo and Russia. Now, no matter what I do (tab-out, click in another cell), the next new row is not added. I believe it should be adding a new row.
Screen 3 : Here the newly added row values have disappeared. That is because I double clicked on the thin border between the two empty cells. Now this is pretty weird.
Screen 4 : Now when I click on the Peter cell, the previously entered row data is back but now it is pushed down and a new empty row is inserted before it. This is very strange.
Can anyone please help me understand this behavior of the DataGrid?
In my case,
First ensure your ItemSource is not using an array that can't add new item to it,
use something like List that can add newItem,
Besides, the SomeClass should have an default constructor takes no parameters like
List<SomeClass>();
public Class SomeClass
{
public SomeClass() { }
}
then the new empty row appear in the bottom of the DataGrid.
Refer to this answer.
I'm going ahead and posting this as an answer here as I need to post a code sample and the comments are starting to become extended (got the invite-to-chat message).
The answer to the original question was ensure that the Type T of the ItemsSource had a parameterless constructor.
Try this code attached to the DataGrid's BeginningEdit event to swallow up the cell border clicks:
private void Grid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
//// Have to do this in the unusual case where the border of the cell gets selected
e.Cancel = true;
}
If you are actually using this handler for something else, or intend to, you can check the OriginalSource to see if it is a Border and cancel the event on that condition.
Use DataGridTextColumn and DataGridComboBoxColumn instead DataGridTemplateColumn, then rows will be added by adequately.
If you want to use DataGridTemplateColumn, then set not only the CellTemplate, but CellEditingTemplate. For example:
<DataGridTemplateColumn Header="Pick a Date">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding myDate}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<DatePicker SelectedDate="{Binding myDate}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>

How to trigger cellTemplateSelector when Items changed

I have 2 templates for DataGrid's CellTemplate. When I change the items, it won't help me select the template for me, my DisplayModeTemplateSelector won't even be called!
What I'm wondering is if there is a way to trigger this CellTemplateSelector again when items changed? How to refresh CellTemplate in DataGrid or ListView When Content Changes
<DataGridTemplateColumn x:Name="colorRange"
Width="*"
Header="Color Range">
<DataGridTemplateColumn.CellTemplateSelector>
<local:DisplayModeTemplateSelector HeatMapTemplate="{StaticResource heatMapTemplate}" ThreshHoldTemplate="{StaticResource threshHoldTemplate}" />
</DataGridTemplateColumn.CellTemplateSelector>
</DataGridTemplateColumn>
I found this blog
http://dotdotnet.blogspot.com/2008/11/refresh-celltemplate-in-listview-when.html
I think this is similar with my problem, but I really can't understand him! Can anyone explain it?
The solution in the blog post will not work with the DataGrid control because the DataGridTemplateColumn class doesn't belong to the Visual Tree, and even when I tried to bind it to a static class, I didn't suceed because of strange exceptions after property changes.
Anyway there is two possible ways to solve this problem.
1) The easier way.
Using the ObservableCollection class.
var itemIndex = 0;
var currentItem = vm.Items[itemIndex];
//Change necessary properties
//..
vm.Items.Remove(currentItem);
vm.Items.Insert(itemIndex, currentItem);
2) The more complex way.
You can add to your item class the property which returns the object itself.
public ItemViewModel(/*...*/)
{
this.SelfProperty = this;
//...
}
public ItemViewModel SelfProperty { get; private set; }
public void Update()
{
this.SelfProperty = null;
this.OnPropertyChanged("SelfProperty");
this.SelfProperty = this;
this.OnPropertyChanged("SelfProperty");
}
After that you can use the ContentControl.ContentTemplateSelector instead of the CellTemplateSelector like this:
<DataGridTemplateColumn Header="Color Range">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentControl Content="{Binding SelfProperty}" ContentTemplateSelector="{StaticResource mySelector}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
And when you change the property, call the Update method somehow:
currentItem.SomeDataProperty = "some new value";
//Or you can add this method call to the OnPropertyChanged
//so that it calls authomatically
currentItem.Update();
The reason why I've set a null value to the SelfProperty in the Update method first, is that the Selector will not update a template until the Content property is completely changed. If I set the same object once again - nothing will happen, but if I set a null value to it first - changes will be handled.
The easy way is to hook the Combo Box's Selection Changed event, and reassign the template selector. This forces a refresh.
In XAML (assume the rest of the DataGrid/ComboBoxColumn:
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource"
Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}, Path=DataContext.Gates, UpdateSourceTrigger=PropertyChanged}"/>
<EventSetter Event="SelectionChanged" Handler="GateIDChanged" />
</Style>
That refers to this DataGridTemplateColumn:
<DataGridTemplateColumn x:Name="GateParamsColumn" Header="Gate Parameters" CellTemplateSelector="{StaticResource GateParamsTemplateSelector}"></DataGridTemplateColumn>
And in the code behind:
private void GateIDChanged(object sender, SelectionChangedEventArgs eventArgs)
{
var selector = GateParamsColumn.CellTemplateSelector;
GateParamsColumn.CellTemplateSelector = null;
GateParamsColumn.CellTemplateSelector = selector;
}

cannot find Name of DataGridColumn programmatically

I found the Columns collection in my datagrid, and was hoping to iterate through it to find a certain column Name. However, I can't figure out how to address the x:Name attribute of the column. This xaml illustrates my problem with a DataGridTextColumn and a DataGridTemplateColumn:
<t:DataGrid x:Name="dgEmployees" ItemsSource="{Binding Employees}"
AutoGenerateColumns="false" Height="300" >
<t:DataGrid.Columns>
<t:DataGridTextColumn x:Name="FirstName" Header="FirstName"
Binding="{Binding FirstName}" />
<t:DataGridTemplateColumn x:Name="LastName" Header="LastName" >
<t:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding LastName}" />
</DataTemplate>
</t:DataGridTemplateColumn.CellTemplate>
</t:DataGridTemplateColumn>
</t:DataGrid.Columns>
</t:DataGrid>
And here is my code:
DataGrid dg = this.dgEmployees;
foreach (var column in dg.Columns)
{
System.Console.WriteLine("name: " + (string)column.GetValue(NameProperty));
}
At runtime, no value is present; column.GetValue doesn't return anything. Using Snoop, I confirmed that there is no Name property on either DataGridTextColumn or DataGridTemplateColumn.
What am I missing?
WPF has two different, yet similar concepts, x:Name, which is used to create a field which references an element defined in XAML, i.e. connecting your code-behind to your XAML, and FrameworkElement.Name, which uniquely names an element within a namescope.
If an element has a FrameworkElement.Name property, x:Name will set this property to the value given in XAML. However, there are instances where it is useful to link non FrameworkElement elements to fields in code-behind, such as in your example.
See this related question:
In WPF, what are the differences between the x:Name and Name attributes?
As an alternative, you could define your own attached property which can be used to name the columns. The attached property is defined as follows:
public class DataGridUtil
{
public static string GetName(DependencyObject obj)
{
return (string)obj.GetValue(NameProperty);
}
public static void SetName(DependencyObject obj, string value)
{
obj.SetValue(NameProperty, value);
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.RegisterAttached("Name", typeof(string), typeof(DataGridUtil), new UIPropertyMetadata(""));
}
You can then assign a name to each column ...
xmlns:util="clr-namespace:WPFDataGridExamples"
<t:DataGrid x:Name="dgEmployees" ItemsSource="{Binding Employees}"
AutoGenerateColumns="false" Height="300" >
<t:DataGrid.Columns>
<t:DataGridTextColumn util:DataGridUtil.Name="FirstName" Header="FirstName"
Binding="{Binding FirstName}" />
<t:DataGridTemplateColumn util:DataGridUtil.Name="LastName" Header="LastName" >
<t:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding LastName}" />
</DataTemplate>
</t:DataGridTemplateColumn.CellTemplate>
</t:DataGridTemplateColumn>
</t:DataGrid.Columns>
</t:DataGrid>
Then access this name in code as follows:
DataGrid dg = this.dgEmployees;
foreach (var column in dg.Columns)
{
System.Console.WriteLine("name: " + DataGridUtil.GetName(column));
}
Hope that helps
You can use linq query to find name of the datagrid column Headers
dgvReports.Columns.Select(a=>a.Header.ToString()).ToList()
where dgvReports is name of the datagrid.

Need to format dates in dynamically built WPF DataGrid

We are binding an unknown result set to a WPF DataGrid at run time. Some of our columns are going to contain DateTime values and we need to properly format these date time fields. Without knowing which columns are going to be DateTime fields at design time, how are we able to format the columns at runtime?
We are using a DataTable's DefaultView to bind to the WPF DataGrid.
Format the binding by StringFormat:
<DataGridTextColumn Header="Fecha Entrada"
Width="110"
Binding="{Binding EnterDate, StringFormat={}\{0:dd/MM/yyyy hh:mm\}}"
IsReadOnly="True" />
I think it's better than writing code behind pieces of code
I figured out how to do this in code...hopefully there is a way to mimic this in XAML. (Please post if you find a working XAML sample.)
To accomplish this in code, add an event handler for the Grid's AutoGeneratingColumn event, such as:
private void ResultsDataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.PropertyType == typeof(DateTime))
{
DataGridTextColumn dataGridTextColumn = e.Column as DataGridTextColumn;
if (dataGridTextColumn != null)
{
dataGridTextColumn.Binding.StringFormat = "{0:d}";
}
}
}
Hey you can set the locale culture info in the constructor of the WPF form as
this.Language = XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag);
Or you can include the xml markup xml:lang="en-GB" in the window header markup
<DataGridTextColumn Header="Last update"
Width="110"
IsReadOnly="True"
Binding="{Binding Path=Contact.TimeUpdate, StringFormat={}\{0:dd/MM/yyyy hh:mm\}, Mode=OneWay}" />
I would use a DataTemplate with a DataType of Date or DateTime (depending on which it will come through as). Place a TextBlock in the DataTemplate with a StringFormat in the binding.
Something like this should work (untested)
<DataTemplate DataType="{x:Type DateTime}">
<TextBlock Text="{Binding StringFormat={0:d}}" />
</DataTemplate>
Or if you want it to apply just in the Grid
<wpfToolkit:DataGrid>
<wpfToolkit:DataGrid.Resources>
<DataTemplate DataType="{x:Type DateTime}">
<TextBlock Text="{Binding StringFormat={0:d}}" />
</DataTemplate>
</wpfToolkit:DataGrid.Resources>
...
</wpfToolkit:DataGrid>
dataGridTextColumn.Binding.StringFormat = "{0:dd/MM/yyyy}";
worked beautifuly
i run this way. its work complete .
<TextBlock Text="{Binding Date_start, StringFormat=\{0:dd-MM-yyyy\}, Mode=OneWay}" />
The answer of FarrEver, May 11 is good, but by me it doens't function. i still get American mm/dd/yyy instead my German dd/mm/yyyy. So I propose to find the regional settings of computer and use it in StringFormat
Private Sub DGrid_AutoGeneratingColumn(ByVal sender As System.Object, ByVal e As Microsoft.Windows.Controls.DataGridAutoGeneratingColumnEventArgs)
If e.PropertyType Is GetType(DateTime) Then
Dim dataGridTextColumn As DataGridTextColumn = TryCast(e.Column, DataGridTextColumn)
If dataGridTextColumn IsNot Nothing Then
Dim ShortDatePattern As String = System.Globalization.DateTimeFormatInfo.CurrentInfo.ShortDatePattern
dataGridTextColumn.Binding.StringFormat = "{0:" + ShortDatePattern + "}" '"{0:dd/MM/yyyy}"
End If
End If
End Sub
see also: my blog

Resources