I have one question regarding standard WPF DataGrid in .NET 4.0.
When I try to set DataGrid grid row height programmaticaly using simple code:
private void dataGrid1_LoadingRow(object sender, DataGridRowEventArgs e)
{
e.Row.Height = 120;
}
everything goes fine till I try to resize grid row on the user interface /standard way on the side using mouse like in excel/ - then it appears grid row can't be resized. It just keep being 120. Its content by the way all goes messed up...
Like Sinead O'Connor would say: tell me baby - where did I go wrong?
You are not meant to set the Height of the row itself as it is resized via the header and such. There is a property, DataGrid.RowHeight, which allows you to do this properly.
If you need to set the height selectively you can create a style and bind the height of the DataGridCellsPresenter to some property on your items:
<DataGrid.Resources>
<Style TargetType="DataGridCellsPresenter">
<Setter Property="Height" Value="{Binding RowHeight}" />
</Style>
</DataGrid.Resources>
Or you can get the presenter from the visual tree (i do not recommend this) and assign a height there:
// In LoadingRow the presenter will not be there yet.
e.Row.Loaded += (s, _) =>
{
var cellsPresenter = e.Row.FindChildOfType<DataGridCellsPresenter>();
cellsPresenter.Height = 120;
};
Where FindChildOfType is an extension method which could be defined like this:
public static T FindChildOfType<T>(this DependencyObject dpo) where T : DependencyObject
{
int cCount = VisualTreeHelper.GetChildrenCount(dpo);
for (int i = 0; i < cCount; i++)
{
var child = VisualTreeHelper.GetChild(dpo, i);
if (child.GetType() == typeof(T))
{
return child as T;
}
else
{
var subChild = child.FindChildOfType<T>();
if (subChild != null) return subChild;
}
}
return null;
}
This works for me.
private void SetRowHeight(double height)
{
Style style = new Style();
style.Setters.Add(new Setter(property: FrameworkElement.HeightProperty, value: height));
this.RowStyle = style;
}
Related
I generate DataTemplate in code to pass it into a GridViewColumn of a GridView of a ListView:
private static DataTemplate CreateTemplate(string sourceProperty)
{
string Xaml = "<DataTemplate xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">" +
" <DataTemplate.Resources>" +
" <Style TargetType=\"DockPanel\">" +
" <Setter Property=\"HorizontalAlignment\" Value=\"Stretch\" />" +
" <Style.Triggers>" +
" <Trigger Property=\"IsMouseOver\" Value=\"True\">" +
" <Setter Property=\"Background\" Value=\"Black\"/>" +
" </Trigger>" +
" <Trigger Property=\"IsMouseOver\" Value=\"False\">" +
" <Setter Property=\"Background\" Value=\"White\"/>" +
" </Trigger>" +
" </Style.Triggers>" +
" </Style>" +
" </DataTemplate.Resources>" +
" <DockPanel x:Name=\"cellPanel\">" +
" <TextBlock Text=\"{Binding " + sourceProperty + "}\"/>" +
" </DockPanel>" +
" </DataTemplate>";
return XamlReader.Parse(Xaml) as DataTemplate;
}
I need to subscribe to mouse events of the DockPaneland cannot do it via parser, since it doesn't work. A workaround I found is to find the DockPanel by name (which is "cellPanel") and subscribe manually. How can I do it? Here is my method for fillinga ListView with columns and templates:
private void FillListView(DataTable table)
{
GridView grid = (GridView)lvMain.View;
foreach (DataColumn col in table.Columns)
{
DataTemplate cellTemplate = CreateTemplate(col.ColumnName);
var gridColumn = new GridViewColumn()
{
Header = col.ColumnName,
CellTemplate = cellTemplate
};
grid.Columns.Add(gridColumn);
}
lvMain.HorizontalContentAlignment = HorizontalAlignment.Stretch;
lvMain.ItemsSource = ((IListSource)table).GetList();
}
I could surely use TemplateFramework.FindName method, but both GridView and GridViewColumn are not FrameworkElements.
The template is not loaded at the point of creation. It will be loaded once the ListViewItems are loaded.
Since the GridViewColumn is just the model for the actual ListViewItem and not part of the visual tree (it doesn't even extend FrameworkElement), you can't access the GridViewColumn or GridViewColumn.CellTemplate directly. The actual cell is placed inside a GridViewRowPresenter of the ListViewItem.
The solution is to iterate over all items, once the ListView is completely loaded and all its items are displayed:
private void FillListView(DataTable table)
{
GridView grid = (GridView)lvMain.View;
foreach (DataColumn col in table.Columns)
{
DataTemplate cellTemplate = CreateTemplate(col.ColumnName);
var gridColumn = new GridViewColumn()
{
Header = col.ColumnName,
CellTemplate = cellTemplate
};
grid.Columns.Add(gridColumn);
}
lvMain.HorizontalContentAlignment = HorizontalAlignment.Stretch;
lvMain.ItemsSource = ((IListSource)table).GetList();
HandleGridViewColumns(lvMain);
}
private void HandleGridViewColumns(ListView listView)
{
foreach (var item in listView.Items)
{
DependencyObject itemContainer = listView.ItemContainerGenerator.ContainerFromItem(item);
// Each item corresponds to one row, which contains multiple cells
foreach (ContentPresenter cellContent in FindVisualChildElements<ContentPresenter>(itemContainer))
{
if (!cellContent.IsLoaded)
{
cellContent.Loaded += OnCellContentLoaded;
continue;
}
SubscribeToDockPanel(cellContent);
}
}
}
private void OnCellContentLoaded(object sender, RoutedEventArgs e)
{
SubscribeToDockPanel(sender as DependencyObject);
}
private void SubscribeToDockPanel(DependencyObject visualParent)
{
if (TryFindVisualChildElementByName(visualParent, "cellPanel", out FrameworkElement element))
{
var dockPanel = element as DockPanel;
// Handle DockPanel
}
}
private IEnumerable<TChild> FindVisualChildElements<TChild>(DependencyObject parent)
where TChild : DependencyObject
{
if (parent is Popup popup)
{
parent = popup.Child;
if (parent == null)
{
yield break;
}
}
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is TChild child)
{
yield return child;
}
foreach (TChild childOfChildElement in FindVisualChildElement<TChild>(childElement))
{
yield return childOfChildElement;
}
}
}
private bool TryFindVisualChildElementByName(DependencyObject parent, string childElementName, out FrameworkElement resultElement)
{
resultElement = null;
if (parent is Popup popup)
{
parent = popup.Child;
if (parent == null)
{
return false;
}
}
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is FrameworkElement uiElement && uiElement.Name.Equals(
childElementName,
StringComparison.OrdinalIgnoreCase))
{
resultElement = uiElement;
return true;
}
if (TryFindVisualChildElementByName(childElement, childElementName, out resultElement))
{
return true;
}
}
return false;
}
The above solution will generally work, but will fail in a scenario where UI virtualization is enabled (which is the default for the ListView). The result of UI virtualization is, that items that are not realized, won't have a item container generated. ItemGenerator.ContainerFromItem would return null in this case, which means the template is not applied and its visual tree therefore not loaded and part of the application tree.
I may update the answer later, to show how to access the item's container template in an UI virtualization context.
But as your primary goal was to attach mouse event handlers to the DockPanel, I recommend a different solution.
UIElement events are Routed Events (which, according to best practice, come in tandem with an instance event wrapper).
Routed Events have the advantage, that they don't require the observer to subscribe to the instance which raises the event. This eliminates code complexity. Instance events introduce the necessity to handle instance life-cycles and their attached event handlers, as instances are created and removed dynamically from/to the visual tree (e.g. TabControl, UI virtualization). Additionally Routed Events don't require any knowledge of the visual tree in order to handle them.
Routed Events work differently. The dependency property system will traverse the visual tree and invoke registered RoutedEventHandler delegates for specific events.
The recommended solution is therefore to subscribe to the required Routed Event e.g. UIElement.PreviewLeftMouseButtonUp.
Because Routed Events are realized by the framework's dependency property system, the listener must be a DependencyObject.
The following example registers an event handler for the UIElement.PreviewLeftMouseButtonUp Routed Event, which will only handle events, that originate from any child element of a DockPanel named CellPanel:
C#
public MainWindow()
{
AddHandler(UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler(OnUIElementClicked));
}
XAML
<MainWindow MouseLeftButtonUp="OnUIElementClicked" />
MainWindow.xaml.cs
private void OnUIElementClicked(object sender, MouseButtonEventArgs e)
{
// Check if the event source is a child of the DockPanel named CellPanel
if (TryFindVisualParentElementByName(e.OriginalSource as DependencyObject, "CellPanel", out FrameworkElement element))
{
// Handle DockPanel clicked
}
}
private bool TryFindVisualParentElementByName(DependencyObject child, string elementName, out FrameworkElement resultElement)
{
resultElement = null;
if (child == null)
{
return false;
}
var parentElement = VisualTreeHelper.GetParent(child);
if (parentElement is FrameworkElement frameworkElement
&& frameworkElement.Name.Equals(elementName, StringComparison.OrdinalIgnoreCase))
{
resultElement = frameworkElement;
return true;
}
return TryFindVisualParentElementByName(parentElement, elementName, out resultElement);
}
Remarks
Since you usually want to do something UI related, when a UI event was raised, I recommend to handle Routed Events using an EventTrigger. They can be defined in XAML, which makes the code more easier to read (as their markup syntax is simpler than the C# syntax) and eliminates the need to manually traverse the visual tree. If you need access to elements of a template, you should move the triggers inside it.
From within the template's scope, you can easily target any named element:
<DataTemplate>
<DataTemplate.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseEnter"
SourceName="CellPanel">
<BeginStoryBoard>
<StoryBoard>
<DoubleAnimation Storyboard.TargetName="CellText"
Storyboard.TargetProperty="Opacity" ... />
</StoryBoard>
</BeginStoryBoard>
</EventTrigger>
</DataTemplate.Triggers>
<DockPanel x:Name="CellPanel">
<TextBlock x:Name="CellText" />
...
</DockPanel>
</DataTemplate>
If you need to do more complex operations, like selecting all text of a TextBox on MouseEnter, you should write an Attached Behavior using attached properties.
I am having an issue when I change my ColumnDefinitions at runtime. When I do, the columns Width does not reflect immediately. If I resize the current window/usercontrol then the Width does reflect.
I am following MVVM as well, I have a DependencyProperty that allows me to change them as needed on the Grid.
Currently here is what I am using and does work if I resize the window and or control the Grid is on...
public static void ColumnSpacerChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (!(obj is Grid) || ((Grid)obj).ColumnDefinitions.Count == 0 || (int)e.NewValue == (int)e.OldValue)
return;
Grid grid = (Grid)obj;
for (int i = 0; i < grid.ColumnDefinitions.Count; i++)
{
if(grid.ColumnDefinitions[i].Tag != null && grid.ColumnDefinitions[i].Tag.ToString() == COLUMN_SPACER)
{
grid.ColumnDefinitions[i] = new ColumnDefinition() { Width = new GridLength((int)e.NewValue, GridUnitType.Pixel), Tag = COLUMN_SPACER };
}
}
grid.UpdateLayout(); // Tried this, but doesn't work
}
I have tried the following and none work.
grid.UpdateLayout();
grid.Refresh; // (off the parent)
Is there something I am missing when I change the ColumnDefinitions during run-time they immediately do not reflect?
I found the solution with some help from #Mike Strobel....
InvalidateArrange()
This needs to be called to invalidate the arrange state (layout) for the element. After this invalidation the element will then have its layout updated and reflected asynchronously.
I'm trying to change the background color of my group headers depending on the value I give them to group after. All my datagrid rows have a "Category" column where I assign different textvalues and I would like to give group headers different colors based on the different values.
Picture of my groups right now:
http://imgur.com/a/JDlAQ
Picture of how I want it to be:
http://imgur.com/a/qdpxW
How can I achieve this?
This work for me:
private void button1_Click(object sender, RoutedEventArgs e)
{
//Retrieve the stackpanel that holds the GroupItems.
StackPanel sp = (StackPanel)GetDescendantByType(listView1, typeof(StackPanel));
//Retrieve the sencond GroupItem;
GroupItem gi = sp.Children[1] as GroupItem;
//Retrieve the Expander inside the GroupItem;
Expander exp = (Expander)GetDescendantByType(gi, typeof(Expander));
//Retrieve the Expander inside the GroupItem;
Expander exp = (Expander)GetDescendantByType(gi, typeof(Expander));
System.Windows.Controls.Primitives.ToggleButton tb = (System.Windows.Controls.Primitives.ToggleButton)GetDescendantByType(exp, typeof(System.Windows.Controls.Primitives.ToggleButton));
Border br = (Border)GetDescendantByType(tb, typeof(Border));
//Change color;
br.Background = Brushes.Red;
br.UpdateLayout();
}
public static Visual GetDescendantByType(Visual element, Type type)
{
if (element == null) return null;
if (element.GetType() == type) return element;
Visual foundElement = null;
if (element is FrameworkElement)
(element as FrameworkElement).ApplyTemplate();
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
Visual visual = VisualTreeHelper.GetChild(element, i) as Visual;
foundElement = GetDescendantByType(visual, type);
if (foundElement != null)
break;
}
return foundElement;
}
Source: How to access group items of a listview in code-behind?
Yo can try wrapping each other with border < Border Background="Black">
See these answers, WPF How to set DataGrid.Group header text when it's actually collapsed
How do I get the value of a cell in a grouped CollectionView?
Apply the converter to the Background of your TextBlock, to change the color based on name.
I currently creating apps in UWP (Universal Windows Platform). I was using pivot control and it seems I couldn't change the pivot item header height! It stays at 68 pixels. I have no idea which element should I modify to change it.
See this image for better explanation (click for full image):
I ran into the same problem but didn't want to use the code behind option.
Using the new Live Tree Debugger in Visual Studio I found that the height comes from the PivotHeaderItem default style.
If you add a copy of that style at an appropriate scope you can set the height using Xaml and you won't need any code.
I blogged about this with a sample project.
No need to copy the entire style for PivotHeaderItem, you can just override it directly in your Template for the Pivot. Here is the relevant section you need to modify from the Sample project.
<!-- While used here to remove the spacing between header items, the PivotHeaderItem template can also be used to
display custom 'selected' visuals -->
<Style TargetType="PivotHeaderItem">
<Setter Property="Height" Value="Auto" />
</Style>
I think i use same template than you.
So, to modify height i set Height on HeaderClipper (see screenshot below) on pivot style :
Hope this will help you.
UPDATE: Because pivot header height is static and can not be more than a small size, we have to set height manually after page is loaded
private void Page_Loaded(object sender, RoutedEventArgs e)
{
foreach (PivotHeaderItem phItem in FindVisualChildren<PivotHeaderItem(mainPivot))
{
phItem.Height = 110;
}
}
//Find all children
public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
ref: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/8c123ae3-2884-4d40-a5d1-0a22355fcd5f/uwpxamlhow-to-increase-pivot-header-height-in-uwp?forum=wpdevelop
I am thinking about converting one of my apps over to WPF, and one of my main concerns was the performance of the DataGrid control (my user's computers are outdated XP machines with no dedicated graphics). I experimented with a few thousand rows and scrolling performance was horrible.
I was wondering if there is a way to set the datagrid cell's content to a custom class that derives from FrameworkElement?
Here is a sample class that appears to have much better performance when put in a virtualizing stackpanel:
public class LiteTextBlock : FrameworkElement
{
private Size _mySize;
private string _myText;
private double totalWidth;
private GlyphTypeface _typeface;
private int _fontSize;
private GlyphRun _run;
public LiteTextBlock(Size mySize, string myText, GlyphTypeface typeface, int fontSize)
{
_mySize = mySize;
this.Width = _mySize.Width;
this.Height = _mySize.Height;
_myText = myText + " additional information";
_typeface = typeface;
_fontSize = fontSize;
}
protected override void OnRender(DrawingContext drawingContext)
{
if (_run == null)
{
totalWidth = 0;
ushort[] glyphIndexes = new ushort[_myText.Length];
double[] advanceWidths = new double[_myText.Length];
for (int i = 0; i < _myText.Length; i++)
{
ushort glyphIndex = _typeface.CharacterToGlyphMap[_myText[i]];
double width = _typeface.AdvanceWidths[glyphIndex] * _fontSize;
if (totalWidth + width >= _mySize.Width - 10)
{
Array.Resize(ref glyphIndexes, i);
Array.Resize(ref advanceWidths, i);
break;
}
glyphIndexes[i] = glyphIndex;
advanceWidths[i] = width;
totalWidth += width;
}
Point origin = new Point(5, 0);
_run = new GlyphRun(_typeface, 0, false, _fontSize, glyphIndexes
, origin, advanceWidths, null, null, null, null, null, null);
}
drawingContext.DrawGlyphRun(Brushes.Black, _run);
}
}
Can anyone show me if this is possible?
Disclaimer: I have already tried using lighter-weight controls such as the ListView, but performance was still poor.
Yes, there is a way.
You need to implement your custom DataGridColumn and override its GenerateElement method and you need to handle DataGrid.AutoGeneratingColumn event to set your custom DataGridColumn to DataGrid.
Here is a sample for custom DataGridColumn:
public class DataGridLiteTextColumn : DataGridColumn
{
private readonly PropertyDescriptor property;
private readonly GlyphTypeface glyphTypeface = new GlyphTypeface(new Uri("file:///C:\\WINDOWS\\Fonts\\Arial.ttf"));
public DataGridLiteTextColumn(PropertyDescriptor property)
{
this.property = property;
}
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
var value = property.GetValue(dataItem);
return new LiteTextBlock(new Size(100, 20), value != null ? value.ToString() : string.Empty, this.glyphTypeface, 10);
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
throw new NotImplementedException();
}
}
Here is a handler for DataGrid.AutoGeneratingColumn event:
private void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
e.Column = new DataGridLiteTextColumn((PropertyDescriptor)e.PropertyDescriptor);
}
This code will work for displaying values, although LiteTextBlock constructor size parameters are probably not set to appropriate values. To edit values you must also implement GenerateEditingElement method.
Make sure the DataGrid's EnableRowVirtualization property is set to True. It should be True by default, but make sure it wasn't set to False in a Style somewhere.
UI Virtualization is enabled by default. But it gets disabled in either of the following scenarios -
The DataGrid has an unlimited height during the measure arrange process.
If the DataGrid thinks it has enough space to arrange all items, then it will not virtualize anything.
This usually happens when you place the DataGrid in a stackpanel.
The ItemsPanel of the datagrid.
The virtualization is accomplished by the VirtualizingStackPanel. If you change the ItemsPanel, the VirtualizingStackPanel will be removed.
Items Grouping.
In .NET4.0, virtualization does not support grouping. In some cases, assign a GroupStyle to the DataGrid will turn off the virtualization even if the items are not grouped.
Please check you are not falling in any of the scenarios mentioned above.
As a side note, you can also implement Data Virtualization on your Items Source of datagrid. Sample for the same with proofs can be found here - Data Virtualization