Programmatically bind DataGridTemplateColumn - c#

Here is my code:
foreach (var columnData in lookup.DataProvider.Metadata)
{
DataGridColumn column = new DataGridTextColumn { Binding = new Binding(columnData.FieldName) };
if (columnData.DataType == typeof(bool))
{
column = new DataGridCheckBoxColumn { Binding = new Binding(columnData.FieldName) };
}
if (columnData.DataType == typeof(DateTime))
{
column = new DataGridTemplateColumn();
//... ????
}
column.Header = columnData.Caption;
DataDataGrid.Columns.Add(column);
}
Basically, I'm creating columns and bindings in code because columns not known at design-time.
Now I need to add templated column and not sure how to write it in C#. Here is example of XAML of column I need to add:
<sdk:DataGridTemplateColumn Header="Received" CanUserReorder="True" CanUserResize="True" CanUserSort="True" Width="Auto" SortMemberPath="SomeTime">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<MyControls:MyDateTimeLabel DisplayUtcDate="{Binding SomeTime}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
EDIT
In case someone interested. I used solution from here: http://www.pettijohn.com/2011/01/silverlight-datagrid-with-dynamic.html
I took version with XAML loader. It definitely smells since I got my namespaces etc hardcoded into strings.
So, I started to explore second choice. Here is how my dynamic column looks now:
column = new DataGridTemplateColumn
{
CanUserSort = true,
SortMemberPath = columnData.FieldName,
CellTemplate = (DataTemplate)this.Resources["DateTimeColumnDataTemplate"]
};
I'm loading DateTemplate from resources. This was cool, but how do I do binding? Suggestion here was to get to my DateTimeLabel and set binding. But that didn't work(see article on why). So, I wrote this code and all is well:
private void OnLoadingRow(object sender, DataGridRowEventArgs e)
{
foreach (DataGridColumn t in this.DataDataGrid.Columns)
{
if (t is DataGridTemplateColumn)
{
var label = t.GetCellContent(e.Row) as DitatDateTimeLabel;
label.SetBinding(DitatDateTimeLabel.DisplayUtcDateProperty, new Binding(t.SortMemberPath));
}
}
}

You could put your DataTemplate inside Page/UserControl resources, retrieve it in code and apply to your column's CellTemplate. It would look sth like this:
column.CellTemplate = (DataTemplate)this.Resources["DateTimeFieldTemplate"];
The binding should work as it is in your DataTemplate XAML right now because on the DataGrid row level your DataContext will be set to the item itself.

Related

Register checked event handler for each checkbox in an auto generated column datagrid

My end goal is to build a datagrid with auto generated columns (because I will never know exactly how many class properties will need to be generated). This datagrid will always have the first column as a checkbox. If a user selects multiple rows in the datagrid, and then checks one of the selected checkboxes, all of the checkboxes (in the first column, or associated column - either is fine) will also be checked.
I've experimented with using a DataGrid.RowHeaderTemplate in xaml like so:
<DataGrid x:Name="SheetListGrid"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HeadersVisibility="All"
AutoGenerateColumns="True"
CanUserResizeRows="False"
AlternatingRowBackground="LightGray"
AutoGeneratingColumn="SheetListGrid_AutoGeneratingColumn">
<DataGrid.RowHeaderTemplate>
<DataTemplate>
<Grid>
<CheckBox Checked="CheckBox_Checked"
IsChecked="{Binding Path=Selected, Mode=TwoWay}"/>
</Grid>
</DataTemplate>
</DataGrid.RowHeaderTemplate>
</DataGrid>
But I had trouble getting multiple checkboxes to switch to checked at once.
My second attempt was to remove the DataGrid.RowHeaderTemplate completely, and place this in a DataGrid AutoGeneratingColumn event handler like so:
Edit: The main purpose of this was to add my own checkbox column and be able to single click the checkbox state.
private void SheetListGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.Column is DataGridCheckBoxColumn && !e.Column.IsReadOnly)
{
FrameworkElementFactory checkboxFactory = new FrameworkElementFactory(typeof(CheckBox));
checkboxFactory.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Center);
checkboxFactory.SetValue(VerticalAlignmentProperty, VerticalAlignment.Center);
checkboxFactory.SetBinding(ToggleButton.IsCheckedProperty, new Binding(e.PropertyName) { UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });
e.Column = new DataGridTemplateColumn
{
Header = e.Column.Header,
CellTemplate = new DataTemplate { VisualTree = checkboxFactory },
SortMemberPath = e.Column.SortMemberPath
};
// notice I tried registering the event handler here as well.
// but this caused a continuous loop of CheckBox_Checked to be
// fired.
// register the checkbox event handler
//checkboxFactory.AddHandler(CheckBox.CheckedEvent, new RoutedEventHandler(CheckBox_Checked));
}
}
Here is my CheckBox_Checked event handler in case that is useful...
private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
// get the selected sheets
var selectedSheets = SheetListGrid.SelectedItems;
if (selectedSheets.Count == 0) return;
foreach (var item in selectedSheets)
{
//DataGridRow row = SheetListGrid.ItemContainerGenerator.ContainerFromItem(item) as DataGridRow;
FakeSheet sheet = item as FakeSheet;
if (sheet.Selected == false)
sheet.Selected = true;
}
SheetListGrid.Items.Refresh();
}
I feel as if I am missing something to make this a whole lot easier, but I can't figure it out and I've nearly melted my RAM from the amount of tabs I have open. I should note that this project will be a class library, and I cannot include any third party assemblies. Additional, I would like to try and avoid using Windows.Interactivity as I've had issues with distributing this class library to others before, and there computer running a separate version which caused errors.

Enable DataGrid sorting for first column in xaml

I have a DataGrid in WPF with autogenerated columns.
How can I disable sorting functionality of all the rows except the first one which corresponds time in my Source.
I am following MVVM pattern and I know that CanUserSortColumns is disabling sorting for all the columns. I want to disable all but the first column.
Should I write trigger or interaction or something else? All the help is appreciated.
<DataGrid AutoGenerateColumns="True"
ItemsSource="{Binding MyDataTable}"
CanUserSortColumns="False">
</DataGrid>
I found this code snippet for you:
<my:DataGridTemplateColumn SortMemberPath="CompleteDate" Header="Complete Date" CanUserSort="true">
<my:DataGridTemplateColumn.CellTemplate >
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<Binding Path="CompleteDate" ConverterCulture="en-GB" StringFormat="{}{0:MM/dd/yyyy}"/>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
</my:DataGridTemplateColumn>
Use the CanUserSort Property to define it's sortable-state column explicit. The SortMemberPath defines the Property you use for sorting. Hope this helps.
UPDATE:
If you are using autogenerated columns you can't access them via xaml. So you need to access them in code File.
I'm not familiar with the xaml Grid but would expect something like:
//Bound Data here so that the Grid generate the columns
int i = 0;
foreach (DataColumn column in myGrid.ColumnCollection)
{
if (i == 0)
column.CanSortUser = true;
else
column.CanSortUser = false;
i++;
}
The original Typenames can differ but something this way should be possible.
UPDATE-2
If you don't want to hurt the MVVM you can use this. There is described how to use Interfaces to access the code and stay independet with your view and viewmodel.
The problem is, that DataGridColumnHeader has the CanUserSort property as a local computed property. It can been set by manually generating the columns (what you don't want).
If it was a property with setter you could create a DataTrigger looking at TabIndex == 0 and set through Setter in xaml Style.
Your only chance by using AutoGeneratedColumns is the following:
<DataGrid AutoGenerateColumns="True"
ItemsSource="{Binding MyDataTable}"
CanUserSortColumns="True"
AutoGeneratedColumns="DataGrid_OnAutoGeneratedColumns">
</DataGrid>
your codebehind:
private void DataGrid_OnAutoGeneratedColumns(object sender, EventArgs e)
{
DataGrid dg = sender as DataGrid;
if (dg == null) return;
dg.Columns.ToList().Select((col, indx) => new {Col = col, Indx = indx}).ToList().ForEach(obj => obj.Col.CanUserSort = obj.Indx == 0);
}
This enables the first columns UserCanSort property and disables the others. This function is independend from your viewmodel and can been placed at your view.xaml.cs code behind file.
EDIT:
If you won't have any codebehind you can just do it by using your own DataGridControl.
MyDataGrid.cs
namespace YourNamespace {
public class MyDataGrid : DataGrid
{
protected override void OnAutoGeneratedColumns(EventArgs e)
{
DataGrid dg = sender as DataGrid;
if (dg == null) return;
dg.Columns.ToList().Select((col, indx) => new {Col = col, Indx = indx}).ToList().ForEach(obj => obj.Col.CanUserSort = obj.Indx == 0);
base.OnAutoGeneratedColumns(e);
}
}
}
your view:
<YourNamespace:MyDataGrid AutoGenerateColumns="True"
ItemsSource="{Binding MyDataTable}"
CanUserSortColumns="True">
</YourNamespace:MyDataGrid>
and you are done without a single line codebehind.

How to set ItemsSource of DataGridTemplateColumn ComboBox in AutoGeneratingColumn?

I am trying to make an auto-generated GridView that will put an editable ComboBox in each cell in each column that contains strings. The selectable items for each column should match the values that are already in that column in the DataTable. I was hoping that something like the code below would work, but I cannot figure out how to set the ItemsSource of the ComboBox dynamically.
private void dataGrid_AutoGeneratingColumn(object sender,
DataGridAutoGeneratingColumnEventArgs e) {
if (e.PropertyType == typeof(string)) {
DataGridTemplateColumn comboColumn = new DataGridTemplateColumn();
comboColumn.Header = e.PropertyName;
comboColumn.CellTemplate = (DataTemplate)Resources["ComboBoxDataTemplate"];
//I'd like to do something like this:
//comboColumn.ItemsSource = (DataContext as MainVM).PartClassVM.DataTable
//.AsEnumerable().Select(row => row[e.PropertyName].ToString()).Distinct();
e.Column = comboColumn;
}
}
Can anyone give me some pointers please?
EDIT: here is the XAML for the DataTemplate:
<DataTemplate x:Key="ComboBoxDataTemplate">
<ComboBox IsEditable="True" Margin="0" />
</DataTemplate>

How to do textwrapping when autogenerate columns is true in DataGrid

I have a DataGrid with autogenerate columns is true because it's columns are getting added dynamically from view model. Still i want to enable the text wrapping because on resizing the columns the text is getting hidden.
XAML Code:
<DataGrid x:Name="individualGrid" Margin="0,2,0,0" Visibility="{Binding ElementName=individualFilter, Path=IsChecked, Converter={StaticResource BoolToVisibility}}"
Grid.Row="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" AutoGenerateColumns="True" VerticalScrollBarVisibility="Auto" Height="500"
ItemsSource="{Binding ElementName=deptFilter, Path=SelectedItem.Individuals.View}" AutomationProperties.AutomationId="AID_UH_individualGrid" ColumnWidth="*" MinColumnWidth="140" />
Please help.
UPDATE 1
I tried the code behind approach by handling auto generating event.It worked but multiple columns of the same type are added.
private void IndividualGrid_OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
//cancel the auto generated column
e.Cancel = true;
//Get the existing column
DataGridTextColumn dgTextC = (DataGridTextColumn)e.Column;
//Create a new template column
DataGridTemplateColumn dgtc = new DataGridTemplateColumn();
DataTemplate dataTemplate = new DataTemplate(typeof(DataGridCell));
FrameworkElementFactory tb = new FrameworkElementFactory(typeof(TextBlock));
tb.SetValue(TextBlock.TextWrappingProperty, TextWrapping.Wrap);
dataTemplate.VisualTree = tb;
dgtc.Header = dgTextC.Header;
dgtc.CellTemplate = dataTemplate;
tb.SetBinding(TextBlock.TextProperty, dgTextC.Binding);
//add column back to data grid
DataGrid dg = sender as DataGrid;
if (dg != null) dg.Columns.Add(dgtc);
}
Can someone suggest why the multiple columns are added whereas it should have only two columns.
UPDATE 2
I finally found a solution by handling autogenerating.
Here is the solution:
private void IndividualGrid_OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
//Get the existing column
DataGridTextColumn dgTextC = (DataGridTextColumn)e.Column;
if (dgTextC != null)
dgTextC.ElementStyle = individualGrid.Resources["wordWrapStyle"] as Style;
}
But now there is an additional issue.There is an extra column at the start looks like an index column. Any suggestions to this?
The only way to achieve what you want is to handle the AutoGeneratingColumn event. From the DataGrid.AutoGenerateColumns Property page on MSDN:
When the AutoGenerateColumns property is set to true, the AutoGeneratingColumn event will be raised for each column that is created. You can change or cancel the column being created in the AutoGeneratingColumn event handler.
<DataGrid Name="DG1" ItemsSource="{Binding}" AutoGenerateColumns="True"
AutoGeneratingColumn="DG1_AutoGeneratingColumn" />
...
//Access and update columns during autogeneration
private void DG1_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
string headername = e.Column.Header.ToString();
//Cancel the column you don't want to generate
if (headername == "MiddleName")
{
e.Cancel = true;
}
//update column details when generating
if (headername == "FirstName")
{
e.Column.Header = "First Name";
}
else if (headername == "LastName")
{
e.Column.Header = "Last Name";
}
else if (headername == "EmailAddress")
{
e.Column.Header = "Email";
}
}
However, you'll basically need to supply a new ItemTemplate for the column to implement text wrapping and you won't be able to do that using this method.
However, there is an additional way of achieving this and that would be for you to override the default Style of the DataGridCell and add a TextBlock that has TextWrapping="Wrap" set on it. Please take a look at the answer to the WPF DataGrid: How do I set columns to TextWrap? question to find out how to do that.
Based on your progress, I found success this way:
// Identify the column from its name in the header
if (e.Column.Header.ToString() == "Column To Wrap")
{
// You can set properties for the column here, personally I
// chose to set the column's width to a good default.
e.Column.Width = DataGridLength.SizeToHeader;
// Cast the column to a DataGridTextColumn
if (e.Column is DataGridTextColumn textColumn)
{
// Create new style based on the existing one. Adding a Setter to it
// that turns on word-wrapping.
var style = new Style(typeof(TextBlock), textColumn.ElementStyle);
style.Setters.Add(new Setter(TextBlock.TextWrappingProperty, TextWrapping.Wrap));
textColumn.ElementStyle = style;
}
}

Binding DataGridTextColumn color from C# codebehind

Similar to WPF - help converting XAML binding expression to codebehind
I am attempting to use Binding to change the color of certain elements in a DataGridTextColumn. Because I need an arbitrary number of DataGrids in separate tabs, I am creating them iteratively in the codebehind. Here is my code for creating the column:
// create a value column
column = new DataGridTextColumn();
column.Binding = new Binding("Value");
BindingOperations.SetBinding(column, DataGridTextColumn.ForegroundProperty, new Binding("TextColor"));
listGrid.Columns.Add(column);
The Value binding works fine, but the TextColor property's getter never gets called.
The grid's ItemsSource property is set to a list of VariableWatcher objects, and here are some of its properties:
public bool Value
{
get { return _variable.Value; }
}
// used to set DataGridTextColumn.Foreground
public Brush TextColor
{
get
{
Color brushColor;
if (_valueChanged)
brushColor = Color.FromRgb(255, 0, 0);
else
brushColor = Color.FromRgb(0, 0, 0);
return new SolidColorBrush(brushColor);
}
}
VariableWatcher implements INotifyPropertyChanged as follows:
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
In one of VariableWatcher's methods, I have the following lines:
_valueChanged = true;
NotifyPropertyChanged("Value");
NotifyPropertyChanged("TextColor");
Stepping over the "Value" line activates a breakpoint in the Value getter, and the Value text is updated in the display. However, stepping over the "TextColor" line does NOT activate the breakpoint in the TextColor getter, and the text color does not change. Any idea what's going on here?
EDIT: Here is the answer, thanks to Damascus. (I would have put this in a comment on his answer, but it wouldn't format my code properly.) I added this to the XAML file:
<Window.Resources>
<Style x:Key="BoundColorStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="{Binding TextColor}" />
</Style>
</Window.Resources>
and in the C# code, I replaced the BindingOperations line with
column.ElementStyle = this.FindResource("BoundColorStyle") as Style;
Workaround for this:
Create a style in your resources, looking like that:
<Style x:Key="MyStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="{Binding TextColor}" />
</Style>
And set this style in the ElementStyle property of your DataGridColumn, should be something like that in your code:
column = new DataGridTextColumn();
column.Style = this.FindResource("MyStyle") as Style;
The reason for it being that ElementStyle directly works in the column's Content (ie. the TextBlock displaying a value)
It can be achieved through code behind as well without style.
The important point is to set binding source explicitly (look in below code) for new binding expression that is set for target property.
In this example, DataGrid generates dynamic columns and is bounded with an object of ColloectionViewSource (i.e cycleDataview) and the source of cycleDataview is an object of ObservableCollection that is cycleRecord.
// Create view source
this.cycleDataview = new CollectionViewSource();
this.cycleDataview.Source = this.cycleRecords;
// Set Item Source to data grid
this.DataGridCycleData.ItemsSource = this.cycleDataview.View;
// Generate Columns for datagrid
var columns = this.cycleRecords.First().CyclePartCols.Select((x, i) => new {PreDescriptor = x.PreDescriptor, Index = i }).ToArray();
foreach (var column in columns)
{
Binding binding = new Binding(string.Format("CyclePartCols[{0}].PartValue", column.Index));
Binding bindingColor = new Binding(string.Format("CyclePartCols[{0}].TextColor", column.Index));
**bindingColor.Source = this.cycleRecords;** // Binding source is required to set
DataGridTextColumn dgc = new DataGridTextColumn();
txtblckCol.Text = column.PreDescriptor;
dgc.Header = txtblckCol;
dgc.Binding = binding;
this.DataGridCycleData.Columns.Add(dgc);
BindingOperations.SetBinding(dgc, DataGridTextColumn.ForegroundProperty, bindingColor);
}
Use the workaround from damascus but only with code behind:
var myStyle = new Style
{
TargetType = typeof(TextBlock)
};
textColumnStyleExt.Setters.Add(new Setter(TextBlock.ForegroundProperty, new Binding("TextColor"));
column = new DataGridTextColumn();
column.Binding = new Binding("Value");
column.ElementStyle = myStyle;

Categories