Get items from GridViewColumn or GridViewColumn.CellTemplate of GridView - c#

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.

Related

How to take Children of a ListView in Code-Behind

I have a ListView with some buttons(created in code-behind). I want to take all theese buttons and place them in a variable:
Button tg = (Button)sender;
ListView st = (ListView) tg.Parent;
var a = st.Children(this function doesn't work for ListView, but it's similar to what should resolve my problem) ;
foreach(Button btn in a)
Since ListView uses data virtualization to have a better performance. See https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/listview-and-gridview-data-optimization. So the buttons may not rendered when they are not in your viewport. This causes you can't get all buttons of your ListView.
But through some trick, we can disable ListView's data virtualization. Note that doing this may causes your app react slow when you have a mount of items in your ListView.
Suppose you want this way.
First, you need to modify ListView's ItemsPanel to StackPanel, according to doc, https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-gridview-and-listview#ui-virtualization.
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
Second, you can get all buttons using a helper function, it returns a List of your controls finded.
public static List<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
List<T> list = new List<T>();
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
list.Add((T)child);
}
List<T> childItems = FindVisualChildren<T>(child);
if (childItems != null && childItems.Count() > 0)
{
foreach (var item in childItems)
{
list.Add(item);
}
}
}
}
return list;
}
Third, you can use it this way.
var listButtons = FindVisualChildren<Button>(listView);
foreach (var btn in listButtons )
{
//to do
}
Done!!!

WPF replace UIElement with another

If I have
<StackPanel>
<Button name="btn"/>
</StackPanel>
how could I replace Button btn for a different control when I press that button? In this case I know the parent is a StackPanel so I could cast the parent to a stackPanel and then replace that child. but what if I have a Border?
<Border>
<Button name="btn"/>
</Border>
Thanks to McGarnagle I created this extension method
how can I replace a Button with my custom control without knowing in advance the parent of the button?
Edit
public static void ReplaceWith(this FrameworkElement elementToReplace, FrameworkElement newControl)
{
newControl.Width = elementToReplace.Width;
newControl.Height = elementToReplace.Height;
newControl.Margin = elementToReplace.Margin;
// get parent of control
var parent = elementToReplace.Parent;
if (parent is Panel)
{
var panel = (Panel)parent;
for (var i = 0; i < panel.Children.Count; i++)
{
if (panel.Children[i] == elementToReplace)
{
panel.Children.RemoveAt(i);
panel.Children.Insert(i, newControl);
break;
}
}
}
else if (parent is Decorator)
{
((Decorator)parent).Child = newControl;
}
else if (parent is ContentControl)
{
((ContentControl)parent).Content = newControl;
}
else
{
if(Debugger.IsAttached)
Debugger.Break();
throw new NotImplementedException("Missing other possibilities to implement");
}
}
I suppose you could cycle through the possible base classes. There aren't that many: StackPanel is a Panel, Border is a Decorator, …
var parent = btn.Parent;
var replacement = new TextBlock { Text = "replacement" };
if (parent is Panel)
{
var panel = (Panel)parent;
panel.Children.Remove(btn);
panel.Children.Add(replacement);
}
else if (parent is Decorator)
{
((Decorator)parent).Child = replacement;
}
else if (parent is ContentControl)
{
((ContentControl)parent).Content = replacement;
}
All containers are Panel or ContentControl or UserControl. So you can check if parent is inherited from one of these controls.
Yo can get the logical parent of Control and check it's type using the operator is like this:
var parent = MyButton.Parent;
if (parent is StackPanel)
{
// do something with StackPanel
}
if (parent is Border)
{
// do something with Border
}

How to access controls inside an ItemTemplate

Using Windows Phone 8, C#.
What I've done is basically done is edited the pivot item. I've named it MainPivot and inside that I've edited the Pivot Item Title and added a TextBlock inside it called PivotTitletxt. XAML for that is:
<DataTemplate x:Key="DataTemplate3">
<TextBlock x:Name="PivotTitletxt" Height="34" TextWrapping="Wrap" Text="{Binding}" Width="447"/>
</DataTemplate>
How can I access this e.g. when setting opacity or changing foreground? so that I can use it on my MainPage like e.g. PivotTitletxt.Opacity = 30; ...
Thanks!
The link #Sankarann gave you is a pretty good example.
I'll try to put it on your scenario:
Your MainPivot has PivotItems right? So What you have to do on the Loaded event is:
var _mainPivot = MainPivot as Pivot
foreach (var _pivotItem in _mainPivot.Items)
{
var _container = _mainPivot.ItemContainerGenerator.ContainerFromItem(_pivotItem);
var _children = AllChildren(_container)
var _name = "PivotTitletxt";
var _control = (TextBlock)_Children.first(x=>x.Name == _name);
_control.Opacity = 30;
}
Then copy the AllChildren method exactly as the it is in the site.
The code above, might have a few adjustments because I've done it without VS...
Hope it helps.
Regards,
============ new answer ==============
Find all controls in WPF Window by type
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;
}
}
}
}
Then try :
TextBlock xx = FindVisualChildren<TextBlock>(mainPivot).FirsOrDefault(x=>x.name=="PivotTitletxt");
if(xx!=null)
xx.opacity = 30
Once again, this come might need some correction...i'm doing it by heart, without VS.
Try it out

Get Children from ListBox can't get children that haven't been viewed

I've got a listbox in WPF like the following XAML. It's full of ListBoxItems that have a checkbox and a label inside of them. One of my items at the top is a "select all" option. When I click the select all option, I have a handler that iterates through all listbox items and it's supposed to check all the checkboxes on all the other listbox children. The problem is that it's only doing the visible children and when it hits the non-visible listboxitems, the VisualTreeHelper seems to be returning null when looking for objects of a specific type (like CheckBox). It seems that VisualTreeHelper seems to be problematic here. Am I using it wrong? Any help appreciated. One other detail - if I scroll and view all listboxitems at least once, it works fine.
mj
XAML - A simple listbox with a ton of children (only the 1st child displayed for brevity)
<ListBox Grid.Row="0" Margin="0,0,0,0" Name="CharacterListBox">
<ListBoxItem>
<StackPanel Orientation="Horizontal">
<CheckBox HorizontalAlignment="Center" VerticalAlignment="Center" Click="AllCharactersClicked"></CheckBox>
<Label Padding="5">All Characters</Label>
</StackPanel>
</ListBoxItem>
C# - Two functions, the first is a helper method which walks the object tree using VisualTreeHelper (I found this on some website). The second function is the click handler for the "select all" listboxitem. It iterates through all children and attempts to check all checkboxes.
private T FindControlByType<T>(DependencyObject container, string name) where T : DependencyObject
{
T foundControl = null;
//for each child object in the container
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(container); i++)
{
//is the object of the type we are looking for?
if (VisualTreeHelper.GetChild(container, i) is T && (VisualTreeHelper.GetChild(container, i).GetValue(FrameworkElement.NameProperty).Equals(name) || name == null))
{
foundControl = (T)VisualTreeHelper.GetChild(container, i);
break;
}
//if not, does it have children?
else if (VisualTreeHelper.GetChildrenCount(VisualTreeHelper.GetChild(container, i)) > 0)
{
//recursively look at its children
foundControl = FindControlByType<T>(VisualTreeHelper.GetChild(container, i), name);
if (foundControl != null)
break;
}
}
return foundControl;
}
private void AllCharactersClicked(object sender, RoutedEventArgs e)
{
MainWindow.Instance.BadChars.Clear();
int count = 0;
foreach (ListBoxItem item in CharacterListBox.Items)
{
CheckBox cb = FindControlByType<CheckBox>(item, null);
Label l = FindControlByType<Label>(item, null);
if (cb != null && l != null)
{
count++;
cb.IsChecked = true;
if (cb.IsChecked == true)
{
string sc = (string)l.Content;
if (sc.Length == 1)
{
char c = Char.Parse(sc);
MainWindow.Instance.BadChars.Add(c);
}
}
}
}
}
Those visual tree walking methods floating around all over the place are a plague. You should almost never need any of that.
Just bind the ItemsSource to a list of objects containing properties for the CheckBoxes, create a data template (ItemTemplate) and bind that property to the CheckBox. In code just iterate over the collection bound to ItemsSource and set the porperty.

Setting DataGrid row height property programmatically

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;
}

Categories

Resources