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?
Related
I am trying to provide a DataGrid column that behaves like the DataGridTextColumn, but with an additional button in editing mode. I looked at DataGridTemplateColumn, but it appears easier to subclass the DataGridTextColumn as below
The problem is the textBox loses its binding when added to the grid. That is, changes to its Text property are not reflected in the non-editing TextBlock or the underlying view-mode
Any thoughts on why this might be and how I can work around it?
public class DataGridFileColumn : DataGridTextColumn
{
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
TextBox textBox = (TextBox)base.GenerateEditingElement(cell, dataItem);
Button button = new Button { Content = "..." };
Grid.SetColumn(button, 1);
return new Grid
{
ColumnDefinitions = {
new ColumnDefinition(),
new ColumnDefinition { Width = GridLength.Auto },
},
Children = {
textBox,
button,
},
};
}
}
I'm using .NET 3.5 and the WPF toolkit
It turns out you also need to override PrepareCellForEdit, CommitCellEdit and CancelCellEdit
The base class assumes (not unreasonably) that the FrameworkElement passed in will be a TextBox
I think you have to set up the binding manually in the GenerateEditingElement(...) method.
Once you've grabbed the TextBox from the base class, set up its binding like this:
textBox.DataContext = dataItem;
textBox.SetBinding(TextBlock.TextProperty, Binding);
This works for me anyway.
Note, I'm not sure why this works, as reading the documentation for GenerateEditingCell implies to me that the TextBox that you grab from the base class should already have its bindings set up properly. However, the above approach is what they did in this blog post.
EDIT:
You don't actually need to set up the binding, it is done already (as it says in the docs). You do need to set up the DataContext though, as for some reason this isn't set up on the textBox returned from the base class.
I'm trying to figure out if its possible to update an IValueConverter through the code behind.
My situation is that I've got two ComboBoxes. Once the first one is updated, I change the ItemsSource property of the second to be one of a variety of enums. I've grabbed an EnumToFriendlyNameConverter from CodeProject, but I'm not sure how to set it.
If I set the converter in the ItemsSource (see below) then it gets ignored when I next set the items source.
ItemsSource="{Binding Converter={StaticResource enumItemsConverter}}"
I found that it is possible by using an ItemTemplate but then I have to manually place in a label, which then has a different style to my other combobox. Getting the styles right just seems like a lot of work...
When you change the ItemsSource you just have to apply the converter again or modify the ItemsSource instead of replacing it.
e.g. create a new binding:
private void ChangeItemsSouce(IEnumerable newItems)
{
Binding binding = new Binding();
binding.Source = newItems;
binding.Converter = new EnumToFriendlyNameConverter();
comboBox.SetBinding(ComboBox.ItemsSourceProperty, binding);
}
Or modify the existing binding:
private void ChangeItemsSouce(IEnumerable newItems)
{
var binding = comboBox.GetBindingExpression(ComboBox.ItemsSourceProperty);
binding.ParentBinding.Source = newItems;
}
How can I determine SelectedCell's Value In DataGrid? (WPF)
My DataGrid has 9 coloums and 5 rows and I want to know the Value of clicked row[0]'s Value.
I used this code in Windows Form:
private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e)
{
var a = dataGridView1[e.ColumnIndex, e.RowIndex].Value;
}
but I don't know an equivalent code in wpf.
You should use DataGrid_SelectedCellsChanged event.
private void dataGrid_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
foreach (var item in e.AddedCells)
{
var col = item.Column as DataGridColumn;
var fc = col.GetCellContent(item.Item);
if (fc is CheckBox)
{
Debug.WriteLine("Values" + (fc as CheckBox).IsChecked);
}
else if(fc is TextBlock)
{
Debug.WriteLine("Values" + (fc as TextBlock).Text);
}
//// Like this for all available types of cells
}
}
HTH
Determining a selected cell's value is more of a WinForms thing. WPF is designed to work differently; your UI is meant to be separated from logic. The DataGrid thus becomes an instrument for presentation, not something to be poked and prodded for values.
Instead, with WPF, you want to deal with the objects you have bound to the grid themselves, independent of how they are displayed. Forget the grid - just find the object that is currently "selected" by the user out of a list of bound objects.
The SelectedItem is a property on the grid itself and thanks to WPF's superior binding mechanisms, you can bind this value to a property on a ViewModel via XAML:
ItemsSource="{Binding Orders, Mode=OneWay}"
SelectedItem="{Binding SelectedOrder, Mode=TwoWay}"
When the user selects an item in the grid, the two-way binding will update the SelectedItem property on the ViewModel with the value of that object in that row.
In that way, you don't even have to deal with the knowledge of the grid or the UI.
I hope that makes sense. I know it's a different approach and a different way of thinking coming from WinForms.
I found a solution posted by others in another thread in stackoverflow: WPF Toolkit DataGrid SelectionChanged Getting Cell Value
Try it.
private void dataGrid1_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
var item = e.AddedCells[0];
{
var col = item.Column as DataGridColumn;
var fc = col.GetCellContent(item.Item);
if (fc is CheckBox)
{
}
else if (fc is TextBlock && col.DisplayIndex == 0)
{
textBlock1.Text = (fc as TextBlock).Text;
}
}
}
sometimes binding to SelectedItem doesn't work (depending how crazy your Model has to be. I have to transpose the model, so everything is upside down and normal defaults don't work all the time.
given that,
in dataGrid selectedCellChanged you could access the bound object by:
assuming from previous example of Orders[] where each Order will have an array of SubOrders
foreach (var selectedCell in e.AddedCells)
{
var order = (Order)selectedCell.Item;
var subOrder = order.SubOrders[selectedCell.Column.DisplayIndex-1];
var someValue = subOrder.Value;
}
I have a DataGridCell that contains a ComboBox.
I want, that when I fire 'SelectionChanged' event of it, a CollectionViewSource of a different column (eventually - at runtime, cell) CellEditingTemplate's Resources should be populated with data according to the selected value for this row.
Maybe DataTrigger, ActionTrigger, EventTrigger, maybe by code, XAML I don't care, I just need a solution.
Thanks a lot!
Related: Accessing control between
DataGridCells, dynamic cascading
ComboBoxes
If I understand your question right, you will fill the contents of a combobox in a cell based on the selection of a combobox in another cell that is in the same row of the DataGrid.
If yes:
First Solution (IMO the preferable)
Make a ViewModel that represents the rows data (a simple wrapper around your data object). Bind the ItemsSource-property of the destination ComboBox to a IEnumerable-property that you provide from your viewmodel.
Bind the SelectedItem from the source-ComboBox to another property of your ViewModel. Every time this source-property changes in your ViewModel, you change the contents of the list that is provided by the ViewModel.
Use for the desintation (list) property a ObservableCollection<T>. The source property is up to you.
Here is an approximately example. I call the class VM (for ViewModel) but this changes nothing on your current solution. MVVM can also be used partial.
public class DataObjectVM : DependencyObject {
public static readonly DependencyProperty SelectedCategoryProperty =
DependencyProperty.Register("SelectedCategory", typeof(CategoryClass), typeof(DataObjectVM), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,delegate (DependencyObject d,DependencyPropertyChangedEventArgs e){
((DataObjectVM)d).SelectedCategoryChanged(e);
}));
ObservableCollection<ItemClass> _items=new ObservableCollection<ItemClass>();
void SelectedCategoryChanged(DependencyPropertyChangedEventArgs e) {
// Change here the contents of the _items collection.
// The destination ComboBox will update as you desire
// Do not change the _items reference. Only clear, add, remove or
// rearange the collection-items
}
// Bind the destination ComboxBox.ItemsSource to this property
public IEnumerable<ItemClass> DestinationItems {
get {
return _items;
}
}
// Bind to this property with the source ComboBox.SelectedItem
public CategoryClass SelectedCategory {
get { return (CategoryClass)GetValue(SelectedCategoryProperty); }
set { SetValue(SelectedCategoryProperty, value); }
}
}
Add a constructor to this class that takes your data object and make some wrapper properties to the rest the properties you need to provide in the DataGrid. If they are alot, you can also make one property that provides your data object and the bind directly to it. Not nice, but it will do the job.
You also can (must) pre-initialize the SelectedCategory with data from your business object. Do this also in the constructor.
As a ItemsSource for the DataGrid you give an IEnumerable of the DataObjectVM-class that wrapps all items you want to show.
Alternative way with VisualTreeHelper
If you want to do it manual, register in the code behind a handler for the ComboBox.SelectionChangedEvent and change then the ItemsSource of the destination ComboBox manual. The business-object you will get with the EventArgs. The destination ComboBox you must search in the visual tree (Use the VisualTreeHelper). The events can be wired also if you use the DataGridTemplateColumn class and add a DataTemplate with the corresponding ComboBoxes.
But I think this is realy not very simple to do and can be error prone. The above solution is much easier.
Here is the code you propably are looking for:
private void CboSource_SelectionChanged(object sender, SelectionChangedEventArgs e) {
ComboBox cbo = (ComboBox)sender;
FrameworkElement currentFe = VisualTreeHelper.GetParent(cbo) as FrameworkElement;
while (null != currentFe && !(currentFe is DataGridRow)) {
currentFe = VisualTreeHelper.GetParent(currentFe) as FrameworkElement;
}
if (null != currentFe) {
List<ComboBox> list = new List<ComboBox>();
FindChildFrameworkElementsOfType<ComboBox>(currentFe,list);
// Requirement 1: Find ComboBox
foreach (ComboBox cboFound in list) {
if (cboFound.Name == "PART_CboDestination") {
// This is the desired ComboBox
// Your BO is available through cbo.Found.DataContext property
// If don't like to check the name, you can also depend on the
// sequence of the cbo's because I search them in a deep search
// operation. The sequence will be fix.
}
}
List<DataGridCell> cells = new List<DataGridCell>();
FindChildFrameworkElementsOfType<DataGridCell>(currentFe,cells);
// Requirement 2: Find Sibling Cell
foreach (DataGridCell cell in cells) {
// Here you have the desired cell of the other post
// Take the sibling you are interested in
// The sequence is as you expect it
DataGridTemplateColumn col=cell.Column as DataGridTemplateColumn;
DataTemplate template = col.CellTemplate;
// Through template.Resources you can access the CollectionViewSources
// if they are placed in the CellTemplate.
// Change this code if you will have an edit cell template or another
// another construction
}
}
}
void FindChildFrameworkElementsOfType<T>(DependencyObject parent,IList<T> list) where T: FrameworkElement{
DependencyObject child;
for(int i=0;i< VisualTreeHelper.GetChildrenCount(parent);i++){
child = VisualTreeHelper.GetChild(parent, i);
if (child is T) {
list.Add((T)child);
}
FindChildFrameworkElementsOfType<T>(child,list);
}
}
And this is the markup I used:
<DataGrid.Columns>
<DataGridTemplateColumn Header="Source" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox Name="PART_CboSource" SelectionChanged="CboSource_SelectionChanged" ItemsSource="!!YOUR ITEMS SOURCE!!" SelectedItem="{Binding Category}">
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Destination">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox Name="PART_CboDestination"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
Accessing the CollectionViewSource
To access the CollectionViewSource, put it into the resources section of the corresponding DataTemplate, not of the panel, then you will have direct access to them. IMO is this location anyway more appropriate than the resources-container of the grid.
If you dont't want to do this, check the state of the following post:
How to get logical tree of a DataTemplate
At run time I want to dynamically build grid columns (or another display layout) in a WPF ListView. I do not know the number and names of the columns before hand.
I want to be able to do:
MyListView.ItemSource = MyDataset;
MyListView.CreateColumns();
You can add columns dynamically to a ListView by using Attached Properties. Check out this article on the CodeProject it explains exactly that...
WPF DynamicListView - Binding to a DataMatrix
From MSDN:
MyListBox.ItemsSource = view;
ListView myListView = new ListView();
GridView myGridView = new GridView();
myGridView.AllowsColumnReorder = true;
myGridView.ColumnHeaderToolTip = "Employee Information";
GridViewColumn gvc1 = new GridViewColumn();
gvc1.DisplayMemberBinding = new Binding("FirstName");
gvc1.Header = "FirstName";
gvc1.Width = 100;
myGridView.Columns.Add(gvc1);
GridViewColumn gvc2 = new GridViewColumn();
gvc2.DisplayMemberBinding = new Binding("LastName");
gvc2.Header = "Last Name";
gvc2.Width = 100;
myGridView.Columns.Add(gvc2);
GridViewColumn gvc3 = new GridViewColumn();
gvc3.DisplayMemberBinding = new Binding("EmployeeNumber");
gvc3.Header = "Employee No.";
gvc3.Width = 100;
myGridView.Columns.Add(gvc3);
//ItemsSource is ObservableCollection of EmployeeInfo objects
myListView.ItemsSource = new myEmployees();
myListView.View = myGridView;
myStackPanel.Children.Add(myListView);
i'd try following approach:
A) you need to have the list box display grid view - i believe this you've done already
B) define a style for GridViewColumnHeader:
<Style TargetType="{x:Type GridViewColumnHeader}" x:Key="gridViewColumnStyle">
<EventSetter Event="Click" Handler="OnHeaderClicked"/>
<EventSetter Event="Loaded" Handler="OnHeaderLoaded"/>
</Style>
in my case, i had a whole bunch of other properties set, but in the basic scenario - you'd need Loaded event. Clicked - this is useful if you want to add sorting and filtering functionality.
C) in your listview code, bind the template with your gridview:
public MyListView()
{
InitializeComponent();
GridView gridViewHeader = this.listView.View as GridView;
System.Diagnostics.Debug.Assert(gridViewHeader != null, "Expected ListView.View should be GridView");
if (null != gridViewHeader)
{
gridViewHeader.ColumnHeaderContainerStyle = (Style)this.FindResource("gridViewColumnStyle");
}
}
D) then in you OnHeaderLoaded handler, you can set a proper template based on the column's data
void OnHeaderLoaded(object sender, RoutedEventArgs e)
{
GridViewColumnHeader header = (GridViewColumnHeader)sender;
GridViewColumn column = header.Column;
//select and apply your data template here.
e.Handled = true;
}
E) I guess you'd need also to acquire ownership of ItemsSource dependency property and handle it's changed event.
ListView.ItemsSourceProperty.AddOwner(typeof(MyListView), new PropertyMetadata(OnItemsSourceChanged));
static void OnItemsSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
MyListView view = (MyListView)sender;
//do reflection to get column names and types
//and for each column, add it to your grid view:
GridViewColumn column = new GridViewColumn();
//set column properties here...
view.Columns.Add(column);
}
the GridViewColumn class itself doesn't have much properties, so you might want to add some information there using attached properties - i.e. like unique column tag - header most likely will be used for localization, and you will not relay on this one.
In general, this approach, even though quite complicated, will allow you to easily extend your list view functionality.
Have a DataTemplateselector to select one of the predefined templates(Of same DataType) and apply the selector on to the ListView. You can have as many DataTemplates with different columns.
You can use a DataTemplateSelector to return a DataTemplate that you have created dynamically in code. However, this is a bit tedious and more complicated than using a predefined one from XAML, but it is still possible.
Have a look at this example: http://dedjo.blogspot.com/2007/03/creating-datatemplates-from-code.html
From experience I can recommend steering clear of dynamic data templates if you can help it... rather use the advice given here to explictly create the ListView columns, rather than trying to create a DataTemplate dynamically.
Reason is that the FrameworkElementFactory (or whatever the class name is for producing DataTemplates at run time) is somewhat cludgey to use (and is deprecated in favor of using XAML for dynamic templates) - either way you take a performance hit.
This function will bind columns to a specified class and dynamically set header, binding, width, and string format.
private void AddListViewColumns<T>(GridView GvFOO)
{
foreach (System.Reflection.PropertyInfo property in typeof(T).GetProperties().Where(p => p.CanWrite)) //loop through the fields of the object
{
if (property.Name != "Id") //if you don't want to add the id in the list view
{
GridViewColumn gvc = new GridViewColumn(); //initialize the new column
gvc.DisplayMemberBinding = new Binding(property.Name); // bind the column to the field
if (property.PropertyType == typeof(DateTime)) { gvc.DisplayMemberBinding.StringFormat = "yyyy-MM-dd"; } //[optional] if you want to display dates only for DateTime data
gvc.Header = property.Name; //set header name like the field name
gvc.Width = (property.Name == "Description") ? 200 : 100; //set width dynamically
GvFOO.Columns.Add(gvc); //add new column to the Gridview
}
}
}
Let's say you have a GridView with Name="GvFoo" in your XAML, which you would like to bind to a class FOO.
then, you can call the function by passing your class "FOO and GridView "GvFoo" as arguments in your MainWindow.xaml.cs on Window loading
AddLvTodoColumns<FOO>(GvFoo);
your MainWindow.xaml file should include the following
<ListView x:Name="LvFOO">
<ListView.View>
<GridView x:Name="GvTodos"/>
</ListView.View>
</ListView>