Overriding DataGridTextColumn - wpf

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.

Related

dynamic tooltips on Winforms controls

I am looking for the cleanest way to bind the same datasource to a control's tooltip that I am binding to the control itself. For example, I have the line
control.DataBindings.Add(new Binding("EditValue", dataFeatures, "Key", true));
where dataFeatures is of type BindingSource. I repeat similar lines for many controls on a WinForm Form. Some of these controls can adopt values whose text can span a greater text width than what is visible within the control itself. Instead of redesigning the layout of the form to account for the possibility of partially hidden text in some controls in a few situations, I would like to have the tooltip of each control be bound to the same property of the BindingSource as the controls' EditValue or Text property. Is this possible? I can imagine there is a way to do it by hand by handling the EditValueChanged event like I already do for different reasons, but I was hoping there would be a cleaner solution than having to add new lines of code for each control.
Anybody have a suggestion?
Thanks!
0. For DevExpress controls you can just bind DevExpressControl.ToolTip property to the same value:
devExpressControl.DataBindings.Add(new Binding("EditValue", dataFeatures, "Key", true));
devExpressControl.DataBindings.Add(new Binding("ToolTip", dataFeatures, "Key", true, DataSourceUpdateMode.Never));
1. For standard WinForms controls you can use System.Windows.Forms.ToolTip component and its ToolTip.Popup event. For each control set its ToolTip to some value otherwise ToolTip will never appears:
control.DataBindings.Add(new Binding("Text", dataFeatures, "Key", true));
toolTip1.SetToolTip(control, "Some value");
Now you can use ToolTip.Popup event:
private bool _updatingToolTip;
private void toolTip1_Popup(object sender, PopupEventArgs e)
{
if (_updatingToolTip) return;
//Get binding for Text property.
var binding = e.AssociatedControl.DataBindings["Text"];
if (binding == null) return;
//Get binding value.
var manager = binding.BindingManagerBase;
var itemProperty = manager.GetItemProperties().Find(binding.BindingMemberInfo.BindingField, true);
object value = itemProperty.GetValue(manager.Current);
string toolTipText;
if (value == null || string.IsNullOrEmpty(toolTipText = value.ToString()))
{
e.Cancel = true;
return;
}
//Update ToolTip text.
_updatingToolTip = true;
toolTip1.SetToolTip(e.AssociatedControl, toolTipText);
_updatingToolTip = false;
}
You can easily implement dynamic tooltips with the ToolTipController component. Put this component onto the Form, and assign to each editor via the BaseControl.ToolTipController property.
When it is done, you can handle the ToolTipController.BeforeShow event and change the text according to the control state. The active control is passed through the SelectedControl property of the event parameter.

Set VisualState of combobox when a item is selected in Xaml (Silverlight)

When my combobox expands and I select an item, I want the combobox to change visual state(it is highlighted). This will signify something is selected. I tried various VisualStates but none of them would trigger in this scenario. How can I achieve this? Thanks.
The standard ComboBox simply doesn't have states to distinguish between having something selected and having nothing selected.
There are a number of ways to go about solving the underlying problem, and it depends mostly on the answer to the following question:
Do you really need to change the visual appearance of the ComboBox itself or does it suffice to style the selected item more prominently?
If it's the latter, you're best served with the rather easy way of using a custom control template for the ComboBoxItems.
If you really want to style the ComboBox itself that way, there are two options I can think of:
A) Add custom states to a ComboBox with a custom template.
Copy your ComboBox's control template and add another state group to the already present states. Both of this is typically done in Expression Blend.
After that you can update the new states in code with
VisualStateManager.GoToState(this, "Selected", true);
for example. You will have to set those states yourself when the first item is chosen. This could be done on the SelectionChanged event.
B) Derive from ComboBox
If you want to use the control in this way often, it might be worthwhile to derive from ComboBox to make your own custom control.
It would look somthing like this:
[TemplateVisualState(Name = "SelectedStates", GroupName = "Unselected")]
[TemplateVisualState(Name = "SelectedStates", GroupName = "Selected")]
// ... (more attributes copied from the ComboBox ones)
public class MyComboBox : ComboBox
{
public MyComboBox()
{
SelectionChanged += HandleSelectionChanged;
DefaultStyleKey = typeof(MyComboBox);
}
void HandleSelectionChanged(object sender, SelectionChangedEventArgs e)
{
VisualStateManager.GoToState(this, SelectedItem != null ? "Selected" : "Unselected", true);
}
}
And you would then need a default style based on the default ComboBox style (or whatever you usually use).
Note that I didn't test this in any way.

Wpf Adorner not responding to interactions

I'm trying to create an overlay in wpf (with darkening background), similar to the ones you can find on the web to popup images.
I would like it to be reusable in more than 1 part of the application, with diffent types of content.
this is the temporary code of the constructor of the adorner class (just to try)
private readonly Grid _grid = new Grid();
public DarkOverlayAdorner(UIElement adornedElement, Object content) :
base(adornedElement)
{
_grid.Background = new SolidColorBrush(Color.FromArgb(99, 0, 0, 0));
IsHitTestVisible = true;
var visual = content as UIElement;
if (visual != null)
_grid.Children.Add(visual);
}
In addition in the class (of course), I have the ovverrides of MeasureOverride and ArrangeOverride to give the adorner the correct size of the adorned element, GetVisualChild, and VisualChildCount...
The problem here is that the adorner is correctly shown, but no events or behaviour are applied on the adorned element. For example:
AdornerLayer layer = AdornerLayer.GetAdornerLayer(textBoxProva);
layer.Add(new DarkOverlayAdorner(textBoxProva, new Button{Content = "prova"}));
The button here is shown, but I can-t click the button and no effects on button mouseover are applied.
I still can't figure out the problem.
Ok, I've lost a lot of time trying to figure out what was the problem.
In the end I found the solution:
If you want the element added to react to events, I think that the element must be bound to the visual tree of the adorner.
The way to do it is to use a VisualCollection, intitialized to the adorner itself:
VisualCollection visualChildren;
FrameworkElement #object;
public DarkOverlayAdorner(UIElement adornedElement) :
base(adornedElement)
{
visualChildren = new VisualCollection(this);
#object = new Button {Content = "prova"};
visualChildren.Add(#object);
}
protected override Visual GetVisualChild(int index)
{
return visualChildren[index];
}
This way the events are correctly routed.
You might want to take a look at the ChildWindow control in the Extended WPF Toolkit. It is a control that pops up a Window with a modal background effect, and you can specify the content to put inside the Window.

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?

how to set all row heights of the wpf datagrid when one row height is adjusted

Im using the wpf datagrid and am looking for a way to set the height on all of the rows when the user adjusts one of them. I know the datagrid has a RowHeight property that sets all of the row heights at once, but the how of catching an individual row height changed escapes me
There aren't any events that could be use directly for that. What you could do is use another event that is fired when you resize the rows and other things. The event I'm thinking of right now is PreviewMouseUp, which is release when you release the mouse button anywhere if you datagrid.
What you could do is when the event is fired, you could check the row height of all of your rows and find the one that is different, then update all rows with it.
I arrived on this by trial and error, so long ass you are using an ItemsSource data source it should work fine.
It should work with virtual rows and causes only a brief visual pause and it switches over (this seems mainly down to column autogeneration so can be avoided).
As hacks go it has the advantage of simplicity and the use of mechanics which are not expected to change.
The heuristic on user triggering of the action might be improved but it has not failed on me yet.
using Microsoft.Windows.Controls;
using Microsoft.Windows.Controls.Primitives;
public static class DataGridExtensions
{
public static void LinkRowHeightsToUserChange(this DataGrid dataGrid)
{
double? heightToApply = null;
bool userTriggered = false;
if (dataGrid.RowHeaderStyle == null)
dataGrid.RowHeaderStyle = new Style(typeof(DataGridRowHeader));
if (dataGrid.RowStyle == null)
dataGrid.RowStyle = new Style(typeof(DataGridRow));
dataGrid.RowStyle.Setters.Add(new EventSetter()
{
Event = DataGridRow.SizeChangedEvent,
Handler = new SizeChangedEventHandler((r, sizeArgs) =>
{
if (userTriggered && sizeArgs.HeightChanged)
heightToApply = sizeArgs.NewSize.Height;
})
});
dataGrid.RowHeaderStyle.Setters.Add(new EventSetter()
{
Event = DataGridRowHeader.PreviewMouseDownEvent,
Handler = new MouseButtonEventHandler(
(rh,e) => userTriggered = true)
});
dataGrid.RowHeaderStyle.Setters.Add(new EventSetter()
{
Event = DataGridRowHeader.MouseLeaveEvent,
Handler = new MouseEventHandler((o, mouseArgs) =>
{
if (heightToApply.HasValue)
{
userTriggered = false;
var itemsSource = dataGrid.ItemsSource;
dataGrid.ItemsSource = null;
dataGrid.RowHeight = heightToApply.Value;
dataGrid.ItemsSource = itemsSource;
heightToApply = null;
}
})
});
}
#Aran
do you remember the rationale behind this?
I can tell you: If you remove both lines to unset and reset the items source (which indeed slows the whole process quite a bit), the row you resize will have its height definitively set.
It seems when you resize a row, you change its Height directly, and this overrides any value you set to the dataGrid's RowHeight property for this row in particular. So basically, here is what you can get :
dataGrid's RowHeight = 20
you change the Height of one Row (say the 5th) to 30 => this row's Height is set to 30 and the dataGrid's RowHeight is set to 30. Everything looking good so far.
now, change another row's Height back to 20 (say the 2nd row). you Set this row's Height to 20 and the DataGrid'RowHeight to 20, which puts all the other rows to 20, EXCEPT the 5th row which stays at 30. (because it had been forced to this value before)
emptying the source and resetting it forces each row to be reloaded and take the dataGrid's RowHeight into account, which eliminates the problem.
As far as I know there is no event that is raised when you resize a row's height.
My first suggestion would be to set the RowStyle in order to create a binding (OneWay) between the the DataGridRow's height property and the datagrid's RowHeight property, but
if you check the Row's height after you resize it, it is unchanged, the ActualHeight is the property that contains the row's "actual" height when you resize it, and ActualHeight cannot be set because "it does not have an accessible set accessor".
After trying this I thought: Where does DataGridRow's ActualHeight gets its value from?
I remembered this post that explains how to detect which cell and row got clicked and also shows the DataGrid's default template visual tree.
By trial and error (using the visual tree image in the link above) I found that it was DataGridCellPresenter that stored the Height that was being used (actually I'm not 100% sure about this, it was the first class that had the height changed up the visual tree since DataGridCell).
Apparently DataGrid doesn't expose the API to get the DataGridCellsPresenter from a DataGridRow (as I found out here)
So my first approach was to get all the DataGridCellPresenter in the DataGrid (through the visual tree) after it has been populated and programatically create a binding between the Height property of the DataGridPresenter and the RowHeight property of the DataGrid.
Here's the code to do that (my DataGrid's instance name is dataGrid1):
Getting all the DataGridCellPresenter:
void GetAllDataGridCellPresenters(DependencyObject parent, List<DataGridCellsPresenter> presenters)
{
int numberOfChildren = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numberOfChildren; i++)
{
if (VisualTreeHelper.GetChild(parent, i) is DataGridCellsPresenter)
{
presenters.Add(VisualTreeHelper.GetChild(parent, i) as DataGridCellsPresenter);
}
else if (VisualTreeHelper.GetChild(parent, i) != null)
{
GetAllDataGridCellPresenters(VisualTreeHelper.GetChild(parent, i), presenters);
}
else
return;
}
}
Setting the bindings programatically on all of them (call this when the Loaded event is raised by the DataGrid):
void SetBindingInDataGridPresenter()
{
List<DataGridCellsPresenter> presenters = new List<DataGridCellsPresenter>();
GetAllDataGridCellPresenters(dataGrid1, presenters);
foreach (DataGridCellsPresenter presenter in presenters)
{
Binding binding = new Binding("RowHeight");
binding.Source = dataGrid1;
binding.Mode = BindingMode.TwoWay;
presenter.SetBinding(DataGridCellsPresenter.HeightProperty, binding);
}
}
(Note: Setting the binding as OneWayToSource didn't work, I really don't know why, I'm probably missing something obvious here...)
This did work... sort of... because I used the Visual Tree to get the DataGridCellsPresenter I only got the visible ones :P, but this shows it can be done this way.
So, finally, the right way to do it would be to supply the DataGrid control template, it can be just as the default one except with the DataGridCellsPresenter's Height property data bound to the RowHeight property of the DataGrid.
I know this does not show exactly how to do it, but you just have to learn (so do I :P)
to redefine a control's template; somehow get the default DataGrid template (or if you're already using another then great, you probably know more than me about it and already know how to do it in order to get the DataGridCellsPresenter Height property automatically bound to the RowHeight DataGrid property) and change it with that bit of magic that gets both height properties bound.

Resources