Avalonia style selector doen't works on derived classes - c#

I'm having an issue to style my custom control derived from button.
I inherited the Button class to add a DependencyProperty to it:
public class IconButton : Button, IStyleable
{
Type IStyleable.StyleKey => typeof(Button);
public static readonly StyledProperty<string> IconProperty =
AvaloniaProperty.Register<IconButton, string>(nameof(Icon));
public string Icon
{
get { return GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
}
Now I want to create a style targetting only this derived class:
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:cc="*****.*****.CustomControls">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Orientation="Vertical">
<cc:IconButton Content="hello custom" Icon="fab fa-github"/>
<Button Content="hello"/>
</StackPanel>
</Border>
</Design.PreviewWith>
<Style Selector=":is(cc|IconButton)">
<Setter Property="Background" Value="Red"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<i:Icon Value="{TemplateBinding Icon}" />
<TextBlock Text="{TemplateBinding Content}" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Styles>
AS you can see, the content is not my control template as the icon is not visible and the background is not red. Therefore I can't use my dependencyproperty.
Any suggestions or is it not supported by the language?
I read on the documentation that is(****) for selector is intended to support derived type so I would expect my code to be working :(
Thx for help.

Try to remove IStyleable and your overriden StyleKey. You are telling the selector that it should look for a Button style and not your style.
Happy coding
Tim
PS I cannot test it on my own right now, so if it doesn't work please tell me here.
Update: I just made a blank new project and tested your code. Once I remove the StyleKey, it works:
This is the modified IconButton:
public class IconButton : Button
{
public static readonly StyledProperty<string> IconProperty =
AvaloniaProperty.Register<IconButton, string>(nameof(Icon));
public string Icon
{
get { return GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
}
If you are able to upload a minimal sample on Github, I can have a look what may be wrong.
Update 2: Looking into your demo App I see that you use a third party icon lib. The lib requires you to configure a special service at start up, see: https://github.com/Projektanker/Icons.Avalonia#1-register-icon-providers-on-app-start-up
I send you a PR which fixes your issue.
Happy coding
Tim

Related

UWP TreeView ItemTemplateSelector not working

The WPF version of this question is here: But it hasn't been answered and I don't know if the UWP TreeView will have the same answer.
I'm trying to add a DataTemplateSelector to the new UWP TreeViews that were just added to windows 10 version 1803 but it isn't working. It is documented here how to use the XAML TreeView Control and even shows how to modify the template to change the Item Datatemplate which works fine. I need to use a datatemplate selector since each of my nodes is using different objects and I need them displayed differently. The TreeView.Node.Content is being set just fine and everything works except it is passing null over to the datatemplateselector in the Object parameter.
Here is my code: (same as the example from Microsoft just with using ItemTemplateSelector)
<Style TargetType="TreeView">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TreeView">
<TreeViewList x:Name="ListControl"
ItemTemplateSelector="{StaticResource CardSelector}"
ItemContainerStyle="{StaticResource TreeViewItemStyle}"
CanDragItems="True"
AllowDrop="True"
CanReorderItems="True">
<TreeViewList.ItemContainerTransitions>
<TransitionCollection>
<ContentThemeTransition />
<ReorderThemeTransition />
<EntranceThemeTransition IsStaggeringEnabled="False" />
</TransitionCollection>
</TreeViewList.ItemContainerTransitions>
</TreeViewList>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Does anyone have any insight or experience on this? My datatemplateselector "CardSelector" works fine and I have been using it in several places without any trouble.
So the point of my question isn't to get anything that I have working but to see if the TreeViewControl works with a DataTemplateSelector. I only have "CardTemplateSelector" in there because I use it in several other places of my app and I know it works. My question is really a "yes, treeview works with a selector" or "no it doesn't" I'm really looking for someone else to try it with their own test template selector and to let me know if they can get it working. Any specific code from me is not relevant to the question. Just see if you can get it to work with whatever selector you want
Yes. The TreeView work well with ItemTemplateSelector.
I used the all code in the document and create a custom class like the following:
public class Test
{
public string Name { get; set; }
}
I made another DataTemplate like this:
<DataTemplate x:Key="TreeViewObjDataTemplate">
<Grid Height="44">
<TextBlock
Text="{Binding Content.Name}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Style="{ThemeResource BodyTextBlockStyle}"/>
</Grid>
</DataTemplate>
My CardTemplateSelector class is the following:
public class CardTemplateSelector: DataTemplateSelector
{
public DataTemplate TreeViewItemDataTemplate { get; set; }
public DataTemplate TreeViewObjDataTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item)
{
TreeViewNode treeViewNode = item as TreeViewNode;
if (treeViewNode.Content is StorageFolder|| treeViewNode.Content is StorageFile)
{
return TreeViewItemDataTemplate;
}
if (treeViewNode.Content is Test)
{
return TreeViewObjDataTemplate;
}
return base.SelectTemplateCore(item);
}
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
return SelectTemplateCore(item);
}
}
I just add the new lines code in MainPage.xaml.cs:
TreeViewNode objnode = new TreeViewNode();
Test test = new Test() {Name="Parent"};
objnode.Content = test;
objnode.IsExpanded = true;
objnode.HasUnrealizedChildren = true;
sampleTreeView.RootNodes.Add(objnode);
The following is the whole xaml page resource code:
<Page.Resources>
<DataTemplate x:Key="TreeViewItemDataTemplate">
<Grid Height="44">
<TextBlock
Text="{Binding Content.DisplayName}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Style="{ThemeResource BodyTextBlockStyle}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="TreeViewObjDataTemplate">
<Grid Height="44">
<TextBlock
Text="{Binding Content.Name}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Style="{ThemeResource BodyTextBlockStyle}"/>
</Grid>
</DataTemplate>
<local:CardTemplateSelector x:Name="CardTemplateSelector" TreeViewItemDataTemplate="{StaticResource TreeViewItemDataTemplate}" TreeViewObjDataTemplate="{StaticResource TreeViewObjDataTemplate}"></local:CardTemplateSelector>
<Style TargetType="TreeView">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TreeView">
<TreeViewList x:Name="ListControl"
ItemTemplateSelector="{StaticResource CardTemplateSelector}"
ItemContainerStyle="{StaticResource TreeViewItemStyle}"
CanDragItems="True"
AllowDrop="True"
CanReorderItems="True">
<TreeViewList.ItemContainerTransitions>
<TransitionCollection>
<ContentThemeTransition />
<ReorderThemeTransition />
<EntranceThemeTransition IsStaggeringEnabled="False" />
</TransitionCollection>
</TreeViewList.ItemContainerTransitions>
</TreeViewList>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Page.Resources>
So far I have answered your question. But I still want to let you know How to ask a good question. In my above comments, I asked you to provide the relevant code, then I could quickly reproduce your question and help you diagnose it. But you said I'm really looking for someone else to try it with their own test template selector and to let me know if they can get it working.. It's Ok. You could see that only I replied. You asked this question for many days. No other community members helped you on this question. That's why I ask you to post some code here. If you provide the relevant code here, I believe many community members will be glad to help you on this question. I really hope you could understand it.
There seems to be confusion about where to apply the DataTemplate. And the all important TargetType is ignored.
If you want a handle on the data item in your custom DataTemplateSelector, you need to:
OPTION 1
Apply the DataTemplateSelector on TreeView.ItemTemplateSelector
Make sure that the DataTemplates have TreeViewNode as the target type.
Only then the data item of the TreeViewNode is supplied to the SetTemplateCore(object item) and SetTemplateCore(object item, DependencyObject container) overrides of your custom DataTemplateSelector.
A working example is found here: Pictures and Music library tree view
OPTION 2
Apply the DataTemplateSelector on TreeViewItem.ContentTemplateSelector
Make sure that the DataTemplates have [YOUR-DATA-TYPE] as the target type
In the TreeView.ItemTemplate bind the DataContext AND Content property to [YOUR-DATA-TYPE], i.e.
<TreeView.ItemTemplate>
<DataTemplate x:DataType="[YOUR-DATA-TYPE]">
<TreeViewItem DataContext="{Binding}" ... Content="{Binding}">
<TreeViewItem.ContentTemplateSelector>
<YourDataTemplateSelector.TemplateA>
<DataTemplate x:DataType="[YOUR-DATA-TYPE]">
...
// YourDataTemplateSelector
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
if (item == null) return null;
return (([YOUR-DATA-TYPE])item).IsSomething ? TemplateA : TemplateB;
}

Logical errors while simulating master-page concept in WPF

I'm trying to simulate the ASP.Net master-page concept (Layout in MVC) on WPF Windows.
I have a CustomWindow class that specifies some behaviors for this sort of Windows:
public class CustomWindow : Window
{
//...
}
And MasterWindowBase; a CustomWindow that takes some sort of my UserControls to be the window content (via style):
public abstract class MasterWindowBase : CustomWindow
{
public MasterWindowBase(MyUserControlBase content)
{
ContentUserControl = content;
Style = Application.Current.FindResource("MasterWindowStyle") as Style;
}
#region ContentUserControl Property
public MyUserControlBase ContentUserControl
{
get { return (MyUserControlBase)GetValue(ContentUserControlProperty); }
set { SetValue(ContentUserControlProperty, value); }
}
public static readonly DependencyProperty ContentUserControlProperty =
DependencyProperty.Register("ContentUserControl", typeof(MyUserControlBase), typeof(MasterWindowBase));
#endregion
}
The style defined in application resources:
<Style x:Key="MasterWindowStyle" TargetType="{x:Type local:MasterWindowBase}">
<Setter Property="Content">
<Setter.Value>
<Grid>
<StackPanel>
<TextBlock Text="This is a master window"/>
<ContentPresenter Content="{Binding ContentUserControl, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MasterWindowBase}}}"/>
</StackPanel>
</Grid>
</Setter.Value>
</Setter>
</Style>
And MasterWindow; a generic window used to create an instance of MasterWindowBase with the specified UserControl type:
public class MasterWindow<TMyUserControlBase>
: MasterWindowBase
where TMyUserControlBase : MyUserControlBase, new()
{
public MasterWindow() : base(new TMyUserControlBase()) { }
}
Now for the first Window.Show, everything works perfectly, but then I caught two logical errors:
When I close the Window and show a new instance of it with a different UserControl, it loads the content of the first-shown MasterWindow.
When I show a new instance of MasterWindow either with the same UserControl or with a different one without closing the currently-showing window(s), it clears the content of all the currently-showing MasterWindow instances, and loads the content of the first-shown MasterWindow in the new instance.
Note that I can't use the Template property inside the MasterWindowStyle style because the style is actually based on CustomWindow's style (in the real project) which already use the Template property.
Any help will be appreciated.
The reason of observed behavior is you try to set Content of your window in Style. Because it's not a template - WPF will create a tree with your UserControl only once. Then when you apply this style again and again - the same visual tree (with the same, first, UserControl) is reused every time (and of course one control cannot be used in different parents - so it gets removed from where it is hosted now and moved to the window you apply style to).
Long story short - you just should not setting Content via Style in WPF. To fix your immediate problem, you can just set ContentTemplate property instead of Content, and wrap what you have in DataTemplate (leaving everything else the same). This will fix it, because for templates new visual tree is created every time.
Here is yet another way to fix it, still using Content property, however it looks like kind of a hack and I'd better void doing this (though still works):
<Application.Resources>
<Grid x:Shared="False" x:Key="myControl">
<StackPanel>
<TextBlock Text="This is a master window"/>
<ContentPresenter Content="{Binding ContentUserControl, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MasterWindowBase}}}"/>
</StackPanel>
</Grid>
<Style x:Key="MasterWindowStyle" TargetType="{x:Type local:MasterWindowBase}">
<Setter Property="Content">
<Setter.Value>
<StaticResource ResourceKey="myControl" />
</Setter.Value>
</Setter>
</Style>
</Application.Resources>
Here you define your visual tree in resources with x:Shared="False" attribute. This attribute means every time this resource is referenced - new instance will be created (by default - same instance is reused). Then you reference this resource inside your Style.

Displaying/styling extra info on new Windows 10 CalendarView

I've recently started development on a Windows 10 app which requires the display of events and extra information on a CalendarView. With the release of the new API, a new CalendarView component was also introduced, so I decided to give it a try. It's a nice widget, but customization has been a hell.
I've gotten to the point where I can display custom information using a ControlTemplate, but binding events and styling with VisualState has been quite a struggle.
This is the ControlTemplate I'm using wrapped in a Style.
<Style x:Key="dayItemStyle" TargetType="CalendarViewDayItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CalendarViewDayItem">
<Grid x:Name="DayItemEventListRoot">
<ListView ItemsSource="{TemplateBinding DataContext}" Padding="20,0,0,0" x:Name="EventInfoList"
IsItemClickEnabled="True" cm:Message.Attach="[Event ItemClick] = [ListTapped]">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel x:Name="EventInfoPanel" Orientation="Horizontal">
<TextBlock x:Name="EventTime" Text="{Binding Date, Converter={StaticResource StringFormatter}, ConverterParameter=\{0:HH:mm\} }"
Foreground="{Binding Foreground, RelativeSource={RelativeSource Self}}" >
</TextBlock>
<TextBlock x:Name="EventDesc" Text="{Binding Name}" Padding="5,0,0,0" Foreground="Black" >
</TextBlock>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="Normal" />
<VisualState x:Name="Today" >
<VisualState.Setters>
<Setter Target="EventTime.Foreground" Value="White" />
<Setter Target="EventDesc.Foreground" Value="White" />
<Setter Target="EventTime.Text" Value="DASFASDDF" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The style is then directly set on the CalenderView component.
<CalendarView Name="FlowCalendar" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalDayItemAlignment="Top" HorizontalDayItemAlignment="Left"
Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="5" Grid.RowSpan="4" CalendarViewDayItemChanging="FlowCalendar_CalendarViewDayItemChanging"
CalendarViewDayItemStyle="{StaticResource dayItemStyle}">
</CalendarView>
Extra information and states are controlled by the CalenderViewDayItemChanging event in the code-behind.
private void FlowCalendar_CalendarViewDayItemChanging(CalendarView sender, CalendarViewDayItemChangingEventArgs args)
{
if(args.Item.Date.Date.Equals(DateTime.Now.Date))
{
VisualStateManager.GoToState(args.Item, "Today", false);
// Testing, gives NullPointerException
//TextBlock bla = (TextBlock) args.Item.FindName("EventTime");
//bla.Text = "SDADASFASDF";
}
if (args.Phase == 0)
{
var eventsByDate = ViewModel.Upcoming.FirstOrDefault(eg => eg.Key.Date == args.Item.Date.Date);
if (eventsByDate != null)
{
args.Item.DataContext = eventsByDate.ToList();
}
}
}
Setting the visual state does nothing (I've checked if it's being called), moving the VisualState(Group) outside of the ControlTemplate just gives me an error that the targets could not be found.
I'm looking to control the event listview style through visual states, which for now are custom since I'm not sure what built-in states CalendarViewDayItem has. I'm quite new to visual states, so any pointers are much appreciated.
Thanks.
Your code will not work because VisualStateManager.GoToState takes a Control as the first parameter; however, your VisualStateManager is defined inside a StackPanel of which type is Panel.
You will need to implement your own VSM which takes a Panel instead. Have a look at the answer to this post on how to do this.
However, this still won't fix your problem. As you need to somehow locate the StackPanels (note 'cause it's within a ListView, there could be multiple) and then call the ExtendedVisualStateManager.GoToState.
I would suggest you to wrap this template within a UserControl (by doing this you might do not even need to extend the VSM as you can use GoToStateAction instead) and have a dependency property (IsToday) to control the states. Then you can use ElementName binding to pass a property at the CalendarViewDayItem level down to IsToday in order to make a state change.
Update
I was actually wrong about the need of using the ExtendedVisualManager to change the visual states. Since it's already inside a UserControl, you can actually call VisualStateManager.GoToState(this, "statename", flag); directly.
However, the way you define the dependency property is wrong.
Replace the code behind with the following and it should work.
public bool IsToday
{
get { return (bool)GetValue(IsTodayProperty); }
set { SetValue(IsTodayProperty, value); }
}
public static readonly DependencyProperty IsTodayProperty =
DependencyProperty.Register("IsToday", typeof(bool), typeof(EventListTemplate), new PropertyMetadata(false, OnIsTodayChanged));
static void OnIsTodayChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var template = (EventListTemplate)d;
if ((bool)e.NewValue)
{
var stateset = VisualStateManager.GoToState(template, "DayItemToday", false);
Debug.WriteLine("did it:", stateset);
}
}

Dependency Property Binding Not Updating Target

I have a custom dependency property:
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register("HeaderProperty", typeof(string), typeof(RadAdjustableSlider));
public string Header
{
get
{
return (string)GetValue(HeaderProperty);
}
set
{
SetValue(HeaderProperty, value);
}
}
I then have a binding in my xaml:
<TextBlock Name="txtHeader" Text="{Binding ElementName=main, Path=Header, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" />
Note that I also have this in the declaration at the top of the xaml file:
x:Name="main"
Finally, I have this constructor:
public RadAdjustableSlider()
{
InitializeComponent();
this.Header = "Header";
}
When I put this control inside of another parent control, the Header textblock is blank. Why?
Edit: This blog says that the correct way to do this is by providing a ValidateValueCallback in the DependencyProperty.Register call, but that seems like quite a bit of plumbing and doesn't explain the way dependency properties behave when interacting with external controls. Am I really going to have to write callback functions for all of my dependency properties?
There is a HeaderedContentControl and HeaderedItemsControl in the framework already...
But if you really want to create your own then you should probably use a TemplateBinding. Try something like this instead:
class MyHeaderedControl : ContentControl
{
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
"Header",
typeof(object),
typeof(MyHeaderedControl),
new PropertyMetadata());
public MyHeaderedControl()
{
this.DefaultStyleKey = typeof(MyHeaderedControl);
}
}
Then in your project create a file at "\Themes\Generic.xaml". This is a specially named file and must be in the root of the project then in the Themes folder. It must contain a ResourceDictionary.
<ResourceDictionary
xmlns="..."
xmlns:x="..."
xmlns:c="MyControlLibrary1"
>
<Style TargetType="{x:Type c:MyHeaderedControl>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type c:MyHeaderedControl}">
<StackPanel>
<ContentControl Content="{TemplateBinding Header}" />
<ContentPresenter />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Also, in your AssemblyInfo.cs add this attribute if it's not there already:
[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly,
ResourceDictionaryLocation.SourceAssembly)]
So for the overview. The general idea is to create some type of logical control where you have properties and events and logic etc. Then in the same assembly you provide default themes. That is how the controls will be displayed by default. At any place where the controls are used the default templates can be overriden and specific templates can be overridden as usual.
So this is the most pain free way you can add custom content like this to your custom controls! Try it once and it will make sense and not feel to cludgy. If you make more controls just keep adding them to the Generic.xaml file.
As justin.m.chase mentioned above, a custom control is probably the best way to go but UserControls are a common scenario so I'll add my 2c anyway.
A UserControl does not set the DataContent property for you and therefore all your bindings inside your UserControl XAML resolve to the DataContent of where you placed the control.
To change this behaviour, either set the DataContext property inside your usercontrol constructor:
public RadAdjustableSlider()
{
InitializeComponent();
this.Header = "Header";
this.DataContext = this;
}
and then bind like this:
<TextBlock Text="{Binding Header}" />
or don't set the DataContext and bind like this:
<TextBlock Text="{Binding Header, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ns:RadAdjustableSlider}}}" />

Passing parameters to a template

Say I have defined a button with rounded corners.
<Style x:Key="RoundButton" TargetType="Button">
<!-- bla bla -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border CornerRadius="0,5,5,0" />
<!-- bla bla -->
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I it possible that the user of this button can specify the CornerRadius? Can I use a TemplateBinding? But where should I bind to? (to Tag?)
In addition to Kent's suggestions, you could also create an attached property to define the CornerRadius on the button, and bind to that property in the template
In order to use a TemplateBinding, there must be a property on the templated control (Button, in this case). Button does not have a CornerRadius or equivalent property, so your options are:
hard code the value in the template
Hijack another property (such as Tag) to store this information. This is quicker, but lacks type safety, is harder to maintain, and prevents other uses of that property.
Subclass Button and add the propery you need, then provide a template for that subclass. This takes a little longer but yields a much nicer experience for consumers of your control.
The button type doesn't have a property for CornerRadius, so templating this won't be possible. I think the easiest way is creating a new class which inherits from Button and add a new dependency property for the CornerRadius. Like this:
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication3
{
public class RoundedButton:Button
{
public CornerRadius CornerRadius
{
get { return (CornerRadius) GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register("CornerRadius", typeof (CornerRadius),
typeof (RoundedButton), new UIPropertyMetadata());
}
}
In xaml you can use it like:
<Local:RoundedButton
Style="{DynamicResource RoundButton}"
Width="64" Height="32"
Content="Hello"
CornerRadius="1,5,10,5"
Background="#FF9CFFD5" />
A template binding to the CornerRadius will work without a problem now.

Categories