Convert DataGrid cell content - wpf

I have a DataGrid whose ItemsSource is set to a DataTable. The DataTable has a column of type DateTime, and I would like to display informational text (ie. "N/A") if the date in a particular cell is a certain value.
My first thought was to somehow bind the cell content to itself, and use a converter, but I can't seem to get it working correctly, and it seems as though there should be a better way.
Additionally, both the DataGrid and DataTable are dynamically generated, so this has to be done in the code behind.
Here's the code I tried initially:
// Create a new DataGridCellStyle
Style myStyle = new Style();
myStyle.TargetType = typeof(DataGridCell);
// Create the binding
Binding myBinding = new Binding();
myBinding.RelativeSource = RelativeSource.Self;
myBinding.Converter = new DateTimeToStringConverter();
// Add the Content setter
Setter mySetter = new Setter();
mySetter.Property = ContentProperty;
mySetter.Value = myBinding;
myStyle.Setters.Add(setter);
// Set the Style and ItemsSource
myDataGrid.CellStyle = myStyle ;
myDataGrid.ItemsSource = myDataTable.DefaultView;
DateTimeToStringConverter does implement IValueConverter, but I'm guessing the problem lies somewhere with the binding, since DateTimeToStringConverter is never actually called when the DataGrid is displayed.

At first, you Add the variable with the name setter to the Setters collection, but you are define the variable with the name mySetter. It may be a reason, why your Converter is not actually called.
Also the solution for your problem will be a bit more complicated.
Actually the Convert get a value of type RowDataView which contains a data for whole row. There is not an information in the converter about a Column or a Cell that is actually binded.
Better will be skip AutoGenerateColumns and generate them programmatically.
Here is example:
myDataGrid.ItemsSource = myDataTable.DefaultView;
myDataGrid.AutoGenerateColumns = false;
foreach (DataColumn column in myDataTable.Columns)
{
Binding binding = new Binding(column.ColumnName)
{
Converter = new DateTimeToStringConverter()
};
DataGridColumn gridColumn = new DataGridTextColumn
{
SortMemberPath = column.ColumnName,
Header = column.ColumnName,
Binding = binding
};
myDataGrid.Columns.Add(gridColumn);
}
Of course, for a performance will be better use the converter only in a DateTime columns.

Related

TextBox inside a listview cell

I have a listview that I would like to add a textbox inside each gridview column cell so I can type data into it and then fetch that data.
I'm creating a datatemplate and passing it to a cell template for the GridViewColumn but when I look at the listview I can't add anything to the cell. It doesn't look like the textbox was even created.
GridViewColumn conceptColumn = new GridViewColumn();
conceptColumn.Header = conceptName;
conceptColumn.CellTemplate = this.GetDataTemplate();
this.TestModeler.Columns.Add(conceptColumn);
conceptColumn.DisplayMemberBinding = new Binding(conceptName);
private DataTemplate GetDataTemplate()
{
DataTemplate dt = new DataTemplate(typeof(TextBox));
FrameworkElementFactory txtElement = new FrameworkElementFactory(typeof(TextBox));
dt.VisualTree = txtElement;
Binding bind = new Binding();
bind.Path = new PropertyPath("Text");
bind.Mode = BindingMode.TwoWay;
txtElement.SetBinding(TextBox.TextProperty, bind);
txtElement.SetValue(TextBox.TextProperty, "test");
return dt;
}
Please take a look at the ListView Class page at MSDN where you can find a XAML example and plenty of link on how to do various things with a WPF ListView.
Of particular interest to you, please take a look at the How to: Use Templates to Style a ListView That Uses GridView page there which explains what you are trying to do (but in XAML) with examples.
MSDN should always be your first place to look as it is full of information just waiting to be read.

WPF - Display Grid of Results With Dynamic Columns/Rows

I'm querying an online service (google data feed) which can return results that will have different numbers of columns and rows with each request.
So far I have been unable to get the data grid or grid to work for me. Ideally I want something that works like excel - you can just add rows and set the values for an individual cell
You can create a class e.g. myGridCol that represents the column and create a collection of the columns. Read the google data feed and create the columns. Then you need to add the columns individually e.g. myGridCol[0], myGridCol[1] .. as DataGridColumns in the code behind. You cannot bind directly to a column collection.
You simply bind to a collection for the rows that has a collection for the columns.
In my case I am using a GridView but I have used the same approach with DataGrid
In my case sDocs is an ObservableCollection
sDoc has public List DocFields
The Fields collection is exactly the same in each sDoc because I made sure it was.
If the Fields collection is not the same in each sDoc then it does not like that.
sDocs is the ItemsSource for the GridView
Then in the code behind I add the columns. As I said before you cannot bind directly to a columns collection. Notice you can even bind the Path to a Property (.e.g. DispValueShort). My class for DocField has other Properties and methods. DocField is actually an Abstract Class with and Abstract Property DispValueShort. Then I have classes for string, date, and enumeration that implement DocField because edit of a string is different from edit of a date. I even have classes for single value and multi value. This is a stable production application.
Binding
<ListView Grid.Row="1" Grid.Column="0" x:Name="lvSrchResulsGrid"
ItemsSource="{Binding Path=MyGabeLib.Search.SDocs}"
Code behind
sDocBaseResultDocsFieldsIndex = 0;
foreach (GabeLib.DocField docField in sDocBaseResultDocsFields)
{
// Debug.WriteLine(" sDocBaseResultDocsFields DispName = " + docField.FieldDef.DispName);
if (fd.FieldDef == docField.FieldDefApplied.FieldDef)
{
gvc = new GridViewColumn();
gvch = new GridViewColumnHeader();
gvch.Content = fd.FieldDef.DispName;
gvch.HorizontalContentAlignment = System.Windows.HorizontalAlignment.Stretch;
if (fd.FieldDef.Sort)
{
gvch.Click += new RoutedEventHandler(SortClick);
gvch.Tag = fd.FieldDef.Name;
}
if (!fd.AppliedDispGrid) gvc.Width = 0; // how to hide
gvc.Header = gvch;
gvBinding = new Binding();
gvBinding.Mode = BindingMode.OneWay;
gvBinding.Path = new PropertyPath("DocFields[" + sDocBaseResultDocsFieldsIndex.ToString() + "].DispValueShort");
template = new DataTemplate();
textblock = new FrameworkElementFactory(typeof(TextBlock));
textblock.SetValue(TextBlock.TextProperty, gvBinding);
textblock.SetValue(TextBlock.TextTrimmingProperty, TextTrimming.WordEllipsis);
// <Setter Property="TextTrimming" Value="WordEllipsis" />
template.VisualTree = new FrameworkElementFactory(typeof(Grid));
template.VisualTree.AppendChild(textblock);
gvc.CellTemplate = template;
gvSearchResults.Columns.Add(gvc);
break;
}
sDocBaseResultDocsFieldsIndex++;
}

DataGridColumn Binding in code

Does anyone know how I can do the equivalent XAML binding in code?
<DataGrid ... >
<DataGrid.Columns>
<DataGridTextColumn
Binding="{Binding Description}" <=== set in code **
/>
</DataGrid.Columns>
</DataGrid>
Cheers,
Berryl
=== UPDATE ====
It looks like the method I have been looking for is DataGridColumn.GenerateElement
If so, then the focus of this question is now how to set the Binding correctly. The reason I want to do this code is that my grid has 7 columns that are identical visually and whose data can be known by an index.
So I want to be able to simplify the xaml by using a subclass DataGridTextColumn which has an index property, and just have:
<DataGrid ... >
<DataGrid.Columns >
<local:DayOfWeekColumn Index="0" />
<local:DayOfWeekColumn Index="1" />
....
<local:DayOfWeekColumn Index="7" />
</DataGrid.Columns >
</DataGrid >
=== REVISED QUESTION ===
Assuming the Binding itself is logically and syntactically correct, what should the parameters to BindingOperations.SetBinding be??
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) {
var activity = (ActivityViewModel)dataItem;
var cellData = activity.Allocations[Index];
var b = new Binding
{
Source = cellData,
UpdateSourceTrigger = UpdateSourceTrigger.LostFocus,
Converter = new AllocationAmountConverter()
};
BindingOperations.SetBinding(??, ??, b);
return ??;
}
=== EDITS for ARAN =====
I am not overriding GenerateElement right now, but rather trying to get a static helper to set my binding for me. The helper is needed in any event to compensate for not being able to bind Header content in the current implementation of MSFT's DataGrid.
Basically the idea is to catch the DC from the grid and use it as necessary on each of the columns, which in this case would be the Header content, cell style, and Binding. Here is the code:
public class TimesheetDataGridColumnContextHelper
{
static TimesheetDataGridColumnContextHelper() {
FrameworkElement.DataContextProperty.AddOwner(typeof (DataGridTextColumn));
FrameworkElement.DataContextProperty.OverrideMetadata(
typeof (DataGrid),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, OnDataContextChanged));
}
public static void OnDataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var grid = d as DataGrid;
if (grid == null || !grid.Name.Equals("adminActivityGrid")) return;
foreach (var col in grid.Columns) {
var dowCol = col as DayOfTheWeekColumn;
if (dowCol == null) continue;
var context = (IActivityCollectionViewModelBase) e.NewValue;
var index = Convert.ToInt32(dowCol.DowIndex);
_setHeader(dowCol, context, index);
var editStyle = (Style) grid.FindResource("GridCellDataEntryStyle");
dowCol.CellStyle = editStyle;
_setBinding(dowCol, index, context);
}
}
private static void _setBinding(DayOfTheWeekColumn dowCol, int index, IActivityCollectionViewModelBase context) {
dowCol.Binding = new Binding
{
Path = new PropertyPath(string.Format("Allocations[{0}]", index)),
UpdateSourceTrigger = UpdateSourceTrigger.LostFocus,
Converter = new AllocationAmountConverter()
};
}
private static void _setHeader(DataGridColumn col, IActivityCollectionViewModelBase context, int index)
{
var date = context.HeaderDates[index];
var tb = new TextBlock
{
Text = date.ToString(Strings.ShortDayOfWeekFormat),
ToolTip = date.ToLongDateString()
};
col.Header = tb;
}
}
}
Everything works except for the Binding. I can't tell if it's because my binding is wrong somehow (although I get no obvious errors) or this is not a good place to set it. The grid columns are just empty when I run it.
Any idea??
Cheers,
Berryl
=== FIXED! ===
The logic in the last update was actually correct, but getting lost in the internals of the DataGrid I missed that my Binding.Path was missing the property to be bound to! Credit to Aran for understanding the issue, realizing that GenerateElement overrides were not necessary, and catching that the Binding Source should not have been set.
You're always doing the fiddly grid bits eh Beryl?
Do a couple of things. Use reflector to look at the implementation of GenerateElement in the DataGridTextColumn. (.NET programmers live in reflector)
Now for the answer:
In the datagrid each column is not part of the visual tree. The column has two methods GenerateElement and GenerateEditingElement. These methods return the viewer and the editor for the cell respectively. In your method above you are not creating the viewer, which will probably be a TextBlock.
from reflector, the implementation of GenerateElement is as below, notice the first thing they do is create the viewer for the cell.
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
TextBlock e = new TextBlock();
this.SyncProperties(e);
base.ApplyStyle(false, false, e);
base.ApplyBinding(e, TextBlock.TextProperty);
return e;
}
Once you have a textblock you can use the line below to set the binding on it.
BindingOperations.SetBinding(textBlock, TextBlock.TextProperty, binding);
I am not however convinced that you actually need to override the GenerateElement and GenerateEditingElement to get your desired effect. I think you could overide the Binding property of the base class and just modify the binding there with your extra field whenever it is set. This will mean everything else will just work and you wont end up removing functionality from your column. Once again a crawl through reflector looking at the class DataGridBoundColumn (the abstract base class) would be beneficial.
I do something similiar in one of our columns whenever a binding is set I modify the clipboard binding by adding an extra property so I can copy and paste effectively.
EDIT: Update...this should probably be another question but..
You are explicitly setting the source of the binding in your setBinding method. In the grid the source of the binding is the data contained in the row. You are setting it, which means it would be the same for each row. You can apply these funky bindings without the source property before the data context is set, the source becomes the item in each row, and your binding should reflect an index into the property held in each row.
Based on MSDN, it sounds like the first parameter of SetBinding() should be the control that you want to display the binding in (this in this case, assuming that GenerateElement() is a member of the DayOfWeekColumn class), and the second property is the property to bind the data to. I haven't used the WPF DataGrid very much, but I didn't see anything like a text property to set.
I do see that the DataGridTextColumn does have a Binding property, though. Maybe it would work to set it to the binding you created manually above?

DataTemplate in listview

I use a listview to display data like a data matrix (columns and rows). My problem is : my items are typed : MatrixCellVM.
I tried everything I found on the net to apply a DataTemplate on this items but nothing worked.
Here is the latest technique I'm using
foreach (var col in dataMatrix.Columns)
{
//create the data template
DataTemplate cellLayout = new DataTemplate();
cellLayout.DataType = typeof(MatrixCellVM);
//set up the stack panel
FrameworkElementFactory spFactory = new FrameworkElementFactory(typeof(Grid));
spFactory.Name = "myComboFactory";
//set up value textblock
FrameworkElementFactory cellValue = new FrameworkElementFactory(typeof(TextBlock));
cellValue.SetBinding(TextBlock.TextProperty, new Binding("Value"));
cellValue.SetValue(TextBlock.ToolTipProperty, "Value");
spFactory.AppendChild(cellValue);
//set the visual tree of the data template
cellLayout.VisualTree = spFactory;
gridView.Columns.Add(new GridViewColumn{
Header = col.Name,
DisplayMemberBinding = new Binding(string.Format("[{0}]", count)),
CellTemplate = cellLayout
});
count++;
}
One more thing, when I override ToString() in MatrixCellVM, the value is displayed (but with this technique I add a context menu or give any color to my value).
I solved the problem but have another one.
So what I did is pretty simple. DisplayMemberBinding and CellTemplate can't be put together.
So I deleted the whole c# code creating the datatemplate.
The only thing that remains is :
gridView.Columns.Add(
new GridViewColumn
{
Header = col.Name,
CellTemplate = (DataTemplate)listView.Resources["MatrixCellVMTemplate"]
});
Thus, I added in my xaml code defining the datatemplate.
My new problem is that my rows are arrays of MatrixCellVM (as it is a matrix, I don't have properties and I don't know by advance how many columns I will have).
That was the point of the DisplayMemberBinding = new Binding(string.Format("[{0}]",count)) code in my top post.
How could I reproduce this behavior in my datatemplate (what is the binding in my textblock).
If anything is not clear, let me know.
Thanks

WPF: Data binding with code

How do I use data-binding from code (C# or VB)?
This is what I have so far, but it is displaying Binding.ToString instead of m_Rep.FirstName.
Public ReadOnly Property TabCaption As Object
Get
Return New Label With {.Foreground = Brushes.Black, .Content = New Binding("FirstName"), .DataContext = m_Rep}
End Get
End Property
Yes, binding in code is a little different from straight assignment (which is how XAML makes it look like it works).
I can give you an example in C# - shouldn't be too far removed from VB.NET.
var label = new Label { Foreground = Brushes.Black, DataContext = m_Rep };
label.SetBinding(Label.ContentProperty, new Binding("FirstName"));
return label;
So the "SetBinding" method binds the "FirstName" path (of the DataContext) to the label's Content property.
You should use m_Rep as a Source of Binding
I have some sample C# code for you as below
Person myDataSource = new Person("Joe");
// Name is a property which you want to bind
Binding myBinding = new Binding("Name");
myBinding.Source = myDataSource;
// myText is an instance of TextBlock
myText.SetBinding(TextBlock.TextProperty, myBinding);
Hope to help

Resources