Validating WPF Visual Tree - c#

I am trying to enforce that any new style added should have to meet some standards. Here is a simple example.
<Style TargetType="{x:Type CellValuePresenter}" BasedOn="{StaticResource {x:Type CellValuePresenter}}">
<Setter Property="Tag" Value="1"></Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border BorderThickness="{TemplateBinding BorderThickness}" />
....
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Updated:
I am trying to validate the style from code behindthat if the style has ControlTemplate, its root should be Border with BorderThickness template binded.
Since Visual Tree is not created until the Control is rendered, I am creating the control at runtime based on target type and applying the style. And I even tried the following
Appropriate way to force loading of a WPF Visual
I can see the visual tree in WPF Tree Visualizer, but I cannot navigate fully using LogicalTreeHelper.GetChildren
I am just experimenting with ideas around:
private void ValidateStyle(Style fieldStyle_, Field field_)
{
if (fieldStyle_.TargetType == typeof(CellValuePresenter))
{
StringBuilder sb = new StringBuilder();
CellValuePresenter presenter = new CellValuePresenter();
presenter.Style = fieldStyle_;
if (fieldStyle_.Setters.Count > 0)
{
foreach (Setter setter in fieldStyle_.Setters)
{
if (setter.Property != null && setter.Property.PropertyType == typeof(ControlTemplate))
{
presenter.ApplyTemplate();
ValidateColumnStyle(0, presenter);
}
}
}
}
}
private void ValidateColumnStyle(int depth_, object obj_)
{
Debug.WriteLine(new string(' ', depth_) + obj_);
if (!(obj_ is DependencyObject))
{
return;
}
if (obj_ is UIElement)
{
Viewbox vb = new Viewbox() {Child = obj_ as UIElement};
vb.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
vb.Arrange(new Rect(vb.DesiredSize));
}
foreach (object child in LogicalTreeHelper.GetChildren(obj_ as DependencyObject))
ValidateColumnStyle(depth_ + 1, child);
}
I am not able to navigate to Border element, any idea how to properly load the control.

Related

Remove ListViewItemPresenter border in Microsoft.UI.Xaml ListView

I'm in the process of converting my UWP app to be a WinUI 3 app. I noticed there's a difference in the way ListViewItems are displayed. The screenshots below show an added Border child to the ListViewItemPresenter in WinUI 3.
UWP
--
WinUI 3
The XAML for both is as simple as it gets:
<ListView>
<ListView.Items>
<ListViewItem>One</ListViewItem>
</ListView.Items>
</ListView>
I'm wondering if there's a way to remove the extra Border element from the WinUI 3 ListViewItemPresenter. I noticed it has a default margin of 4, 2, 4, 2, which causes visual differences relative to my existing UWP app.
I'm not sure if you can remove the Border inside the ListViewItemPresenter but you can modify its Margin like this:
MainPage.xaml
<Grid>
<ListView x:Name="CustomListView">
<ListView.Items>
<ListViewItem>One</ListViewItem>
<ListViewItem>Two</ListViewItem>
<ListViewItem>Three</ListViewItem>
</ListView.Items>
</ListView>
</Grid>
MainPage.xaml.cs
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
this.Loaded += MainPage_Loaded;
}
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
foreach (ListViewItem listViewItem in this.CustomListView.FindChildrenOfType<ListViewItem>())
{
if (listViewItem
.FindChildrenOfType<ListViewItemPresenter>()
.FirstOrDefault() is ListViewItemPresenter listViewItemPresenter &&
listViewItemPresenter
.FindChildrenOfType<Border>()
.FirstOrDefault() is Border border)
{
border.Margin = new Thickness(0, 0, 0, 0);
}
}
}
}
Extensions.cs
public static class Extensions
{
public static IEnumerable<T> FindChildrenOfType<T>(this DependencyObject parent) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (child is T childOfT)
{
yield return childOfT;
}
foreach (T grandChild in child.FindChildrenOfType<T>())
{
yield return grandChild;
}
}
if (parent is ContentControl contentControl)
{
if (contentControl.Content is T contentOfT)
{
yield return contentOfT;
}
if (contentControl.Content is DependencyObject dependencyObjectContent)
{
foreach (T grandChild in dependencyObjectContent.FindChildrenOfType<T>())
{
yield return grandChild;
}
}
}
}
This works if you have pre-fixed items in the ListView. If you need to add items after the Loaded event, you need to apply this for the new items.
I'm also struggling with the same issue, I've got one way which might be a little bit nicer depending on your preferences of course. Just get the styles of the listviewitempresenter from the themeresources file https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/CommonStyles/ListViewItem_themeresources.xaml then add a negative margin Margin="-4,-2.4,-4,-2.4" to neutralize the margin of the border. Still not a nice way and still looking for a better ways...
<ItemsControl.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<ListViewItemPresenter Margin="-4,-2.4,-4,-2.4" ContentTransitions="{TemplateBinding ContentTransitions}"
Control.IsTemplateFocusTarget="True"
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
FocusVisualPrimaryBrush="{TemplateBinding FocusVisualPrimaryBrush}"
FocusVisualPrimaryThickness="{TemplateBinding FocusVisualPrimaryThickness}"
FocusVisualSecondaryBrush="{TemplateBinding FocusVisualSecondaryBrush}"
FocusVisualSecondaryThickness="{TemplateBinding FocusVisualSecondaryThickness}"
SelectionCheckMarkVisualEnabled="{ThemeResource ListViewItemSelectionCheckMarkVisualEnabled}"
CheckBrush="{ThemeResource ListViewItemCheckBrush}"
CheckBoxBrush="{ThemeResource ListViewItemCheckBoxBrush}"
DragBackground="{ThemeResource ListViewItemDragBackground}"
DragForeground="{ThemeResource ListViewItemDragForeground}"
FocusBorderBrush="{ThemeResource ListViewItemFocusBorderBrush}"
FocusSecondaryBorderBrush="{ThemeResource ListViewItemFocusSecondaryBorderBrush}"
PlaceholderBackground="{ThemeResource ListViewItemPlaceholderBackground}"
PointerOverBackground="{ThemeResource ListViewItemBackgroundPointerOver}"
PointerOverForeground="{ThemeResource ListViewItemForegroundPointerOver}"
SelectedBackground="{ThemeResource ListViewItemBackgroundSelected}"
SelectedForeground="{ThemeResource ListViewItemForegroundSelected}"
SelectedPointerOverBackground="{ThemeResource ListViewItemBackgroundSelectedPointerOver}"
PressedBackground="{ThemeResource ListViewItemBackgroundPressed}"
SelectedPressedBackground="{ThemeResource ListViewItemBackgroundSelectedPressed}"
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
ReorderHintOffset="{ThemeResource ListViewItemReorderHintThemeOffset}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
ContentMargin="{TemplateBinding Padding}"
CheckMode="{ThemeResource ListViewItemCheckMode}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>

Accessing Elements or Controls in a UWP ControlTemplate

I have a XAML UserControl that inherits from the CalendarView element. In the CalendarViewItemStyle, I have edited the ControlTemplate to hold a Grid and a TextBox.
<Style x:Name="CalDayStyle"
TargetType="CalendarViewDayItem">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate x:Name="cTemp">
<Grid x:Name="RootGrid"
Background="{TemplateBinding Background}">
<StackPanel x:Name="CalDayStack"
Orientation="Vertical"
VerticalAlignment="Stretch">
<TextBlock x:Name="tasksPres"
TextAlignment="Center"
HorizontalAlignment="Stretch">
</TextBlock>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I am trying to access the TextBox but to no avail, using FindName(). What do I do.
FindName cannot find names that are defined in applied templates. To find items in applied templates, use VisualTreeHelper.GetChild to get the applied template root object. Then you can call FindName on that root object, and you will be searching the XAML namescope of the template rather than the greater page.
If you want to get the TextBlock in your Style, you should be able touse the VisualTreeHelper to get it.
For examole:
public MainPage()
{
this.InitializeComponent();
texts = new List<TextBlock>();
}
private List<TextBlock> texts;
private void Button_Click(object sender, RoutedEventArgs e)
{
IEnumerable<TextBlock> textBlocks = FindVisualChildren<TextBlock>(Mycontrol);
foreach (var textBlock in textBlocks)
{
if (textBlock.Name == "tasksPres")
{
texts.Add(textBlock);
}
}
foreach (var item in texts)
{
item.Text = "11111111111";
}
}
private 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;
}
}
}
}

create a button programmatically with styles

Simple syntax question. Programming silverlight 4 on VS2010. I created a button style in xaml:
<UserControl.Resources>
<Style x:Key ="TestbuttonStyle" TargetType="Button">
<Setter Property="Width" Value="150"></Setter>
<Setter Property="Margin" Value="0,0,0,10"></Setter>
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Image Source="http://i40.tinypic.com/j5k1kw.jpg" Height="20" Width="20" Margin="-30,0,0,0"></Image>
<TextBlock Text="sampleuser
sample number" Margin="5,0,0,0"></TextBlock>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
I need to create a button in the code behind, but using this style. I tried doign something like this:
Button btn = new Button();
//btn.Style = {TestbuttonStyle}; -what do i put here?
grid.children.add(btn);
how to I apply the style and add to my usercontrol grid?
Initially I thought you were working with WPF. Then I realized it's about Silverlight which doesn't have a hierarchical resource look-up helper method similar to WPF's FindResource or TryFindResource, respectively.
However, a quick search on the internet gave up this article which describes a nice extension method you can use:
public static object TryFindResource(this FrameworkElement element, object resourceKey)
{
var currentElement = element;
while (currentElement != null)
{
var resource = currentElement.Resources[resourceKey];
if (resource != null)
{
return resource;
}
currentElement = currentElement.Parent as FrameworkElement;
}
return Application.Current.Resources[resourceKey];
}
Then you can use it like this:
btn.Style = (Style)this.TryFindResource("TestbuttonStyle");

Add Items to ListBox and Style

I have a simple class:
public class Foo
{
public string Text { get; set; }
public bool AppleStyle { get; set; }
public Foo(string text, bool applyStyle)
{
Text = text;
ApplyStyle = applyStyle;
}
public override string ToString()
{
return Text;
}
}
Which is then used to add items to a ListBox:
var one = new Foo("Some Text", false);
var two = new Foo("More Text", true);
MyListBox.Items.Add(one);
MyListBox.Items.Add(two);
I then loop through the items in the ListBox to figure out how to style them. This is where I get stuck. I tried inheriting from ListBoxItem for the class, but no items get added if I do that.
for (int i = 0; i < MyListBox.Items.Count; i++)
{
if(((Foo)MyListBox.Items[i]).ApplyStyle)
{
((ListBoxItem)MyListBox.Items[i]).Style = Resources["MyStyle"] as Style;
}
}
Update:
In MainWindow.xaml:
<Window.Resources>
<Style x:Key="MyStyle" TargetType="ListBoxItem">
<Setter Property="Background" Value="Bisque"></Setter>
<Setter Property="FontWeight" Value="Bold"></Setter>
</Style>
</Window.Resources>
Update 3:
Making some progress, just need to know how to refresh the styles (after clicking on a button). Plus if Resource is not in MainWindow.xaml, would it then look in App.xaml before returning null?
MainWindow.xaml
<Window...>
<Window.Resources>
<Style x:Key="MyClass" TargetType="ListBoxItem">
<Setter Property="Background" Value="Bisque"></Setter>
<Setter Property="FontWeight" Value="Bold"></Setter>
</Style>
<myapp:MyListItemStyleSelector x:Key="MyListItemStyleSelector" />
</Window.Resources>
<Grid>
...
<ListBox .... ItemContainerStyleSelector="{StaticResource: MyListItemStyleSelector}" />
...
</Grid>
</Window>
MyListItemStyleSelector.cs
public class MyListItemStyleSelector : StyleSelector
{
public override Style SelectStyle(object item, DependencyObject container)
{
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(container);
int index = ic.ItemContainerGenerator.IndexFromContainer(container);
Style applyStyle = null;
var data = item as Foo;
if (data != null && data.ApplyStyle)
{
applyStyle = ic.TryFindResource("MyStyle") as Style;
}
return applyStyle;
}
}
I think you have some sort of mixup here, i try to explain as good as i can.
First of all You usually never need to change the Style in code, like your last code block.
One thing that is difficult to understand in the beginning is the use of a ItemContainerStyle and DataTemplate.
I would suggest that you do the following.
Instead of changing the style off your ListBoxItem see if it is sufficient to use a DataTemplate. The DataTemplate defines how the Content of your ListBoxItem is shown.
<DataTemplate TargetType="{x:Type Foo}">
<!-- your visuals and controls here -->
</DataTemplate>
Now if you want to use different datatemplates you could use different classes and create different DataTemplates for them, or you use a DataTemplateSelector
public class FooTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
FrameworkElement element = container as FrameworkElement;
var mdl = item as Foo;
if( mdl.AppleStyle )
return element.FindResource("appleTemplate") as DataTemplate;
return element.FindResource("normalTemplate") as DataTemplate;
}
}
Create that templateselector in xaml and reference it in your listbox
<myNs:FooTemplateSelector x:Key="fooTemplateSelector"/>
<Listbox DataTemplateSelector="{StaticResource fooTemplateSelector}"/>
now you need to create 2 DataTemplates appleTemplate *normalTemplate* and you can easyl distinguish which data template to use vial the selector. Which is done automatically in the ListBox for you.
If you really want to change the Style of the ItemContainer you can use ItemContainerStyleSelector which works similar to the DataTemplateSelector. But i would not suggest it. You should supply the content and leave the ListBoxItem as it is, only if you want to modify the design(in this case, the selection color etc.), otherwise it might confuse the user or break functionality.
If you add data-objects directly to the ListBox the container-items will be generated automatically, you cannot get them this way.
Use the ItemContainerGenerator:
((ListBoxItem)MyListBox.ItemContainerGenerator.ContainerFromIndex(i)).Style = Resources["MyStyle"] as Style;
Why not do this in the XAML?
<ListBox Name="MyListBox">
<ListBox.Resources>
<Style TargetType="{x:Type ListBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding ApplyStyle}" Value="True">
<Setter Property="Background" Value="Bisque" />
<Setter Property="FontWeight" Value="Bold" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Resources>
</ListBox>
But your overall problem is that ListBox.Items returns a collection of data objects, not XAML Controls. To get the XAML control that contains the Data Object you have to do as H.B. suggested and use MyListBox.ItemContainerGenerator.ContainerFromItem(dataObject) to get the XAML Container for the data object. Just be sure you wait until after the ItemContainerGenerator has finished rendering items to get the container (I believe it has a Status property or StatusChanged event you can use... it's been a while and I can't remember the exact syntax)

ItemsControl: How To Use FindName within ItemsPanelTemplate to access Panel

<Style TargetType="{x:Type local:CustomItemsControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<ScrollViewer>
<ItemsPresenter x:Name="PART_Presenter"/>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel x:Name="PART_StackPanel" IsItemsHost="True"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
Trying to access the StackPanel to set Events when children are changed.
[TemplatePartAttribute(Name = "PART_StackPanel", Type = typeof(StackPanel))]
[TemplatePartAttribute(Name = "PART_Presenter", Type = typeof(ItemsPresenter))]
public class CustomItemsControl: ItemsControl
{
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var presenter = (ItemsPresenter)this.Template.FindName("PART_Presenter", this);
var stackPanel = (StackPanel)this.ItemsPanel.FindName("PART_StackPanel",this);
}
}
Get Exception when I try to locate the StackPanel.
InvalidOperationException:
This operation is valid only on elements that have this template applied.
Please advise if there is a way to find a TemplatePart within an ItemsPanelTemplate. And when should I expect to know when the ItemsPanelTemplate is applied?
Another option is to call .ApplyTemplate() on the ItemsPresenter while still in the ItemControl's OnApplyTemplate method. Then the call to .FindName will succeed.
[TemplatePartAttribute(Name = "PART_StackPanel", Type = typeof(StackPanel))]
[TemplatePartAttribute(Name = "PART_Presenter", Type = typeof(ItemsPresenter))]
public class CustomItemsControl : ItemsControl
{
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var presenter = (ItemsPresenter)this.Template.FindName("PART_Presenter", this);
presenter.ApplyTemplate();
var stackPanel = (StackPanel)this.ItemsPanel.FindName("PART_StackPanel", presenter);
}
}
Figured out that the Loaded event was the one to wait for on an ItemsPanelTemplate. I am able to find the StackPanel using the TemplatePart Name. Thanks to Rick for suggesting the StackPanel should be found within the Presenter.
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
this.Loaded += new Accordion_Loaded;
}
void Accordion_Loaded(object sender, RoutedEventArgs e)
{
var presenter = (ItemsPresenter)this.Template.FindName("PART_Presenter", this);
var stackPanel = (StackPanel)this.ItemsPanel.FindName("PART_StackPanel", presenter);
}
The FindName method only finds names in a template that has been expanded and the ItemsPanel is expanded by the ItemsPresenter, not the ItemsControl. In your situation the "PART_StackPanel" will always be the child of the "PART_Presenter" so you can get a reference to it like this:
var stackPanel = (StackPanel)VisualTreeHelper.GetChild(presenter, 0);

Categories