HateDread
Topic Author
Posts: 72
Joined: 06 Feb 2020, 10:27

Attached Properties in templates/styles and used in binding?

13 Jan 2024, 04:50

I have a situation with a few templates/styles that sometimes need one type of binding path prefix and sometimes another. I have a base ObjectIconBase control, and it branches on the presence of specific components to figure out if it's a specific type of icon that needs direct entity component access (using ["ComponentName"] format), or needs a redirect to another entity, e.g. the ViewModel is actually for a tree node wrapping something more like the former, but eventually needs to get into that same accessor.

That is to say that sometimes, I'm looking for this binding, using [] directly:
    <ControlTemplate x:Key="ObjectIconOverrideTemplate">
        <Grid>
            <Image Source="{Binding [IconOverrideComponent].IconName}" Stretch="Uniform" Height="Auto" Width="Auto"/>
            <Rectangle Height="Auto" Width="Auto" Stretch="Uniform" Style="{StaticResource ObjectIconColourStyle}">
                <Rectangle.OpacityMask>
                    <ImageBrush ImageSource="{Binding [IconOverrideComponent].IconName}"/>
                </Rectangle.OpacityMask>
            </Rectangle>
        </Grid>
    </ControlTemplate>
And sometimes this, with a redirect to the target entity that this tree node is wrapping (the node is a FleetNodeEntityViewModel, also an EntityViewModel, but when that UI node VM is representing an underlying ship in the world, we want to be able to selectively drill down to that underlying EntityViewModel and check its components instead, rather that duplicating every style/template to have one for each pathway)
    <ControlTemplate x:Key="ObjectIconOverrideTemplate">
        <Grid>
            <Image Source="{Binding SomeEntityRedirectPropertyHere[IconOverrideComponent].IconName}" Stretch="Uniform" Height="Auto" Width="Auto"/>
            <Rectangle Height="Auto" Width="Auto" Stretch="Uniform" Style="{StaticResource ObjectIconColourStyle}">
                <Rectangle.OpacityMask>
                    <ImageBrush ImageSource="{Binding SomeEntityRedirectPropertyHere[IconOverrideComponent].IconName}"/>
                </Rectangle.OpacityMask>
            </Rectangle>
        </Grid>
    </ControlTemplate>
In trying to make this configurable, I'm using Attached Properties, initially I tried using a string, hoping I could build the above example in a kind of 'entityPathProperty + "[IconOverrideComponent].IconName"' way, but obviously you can't get that kind of dynamicism in bindings, so now I'm trying to just expose my entity, as an EntityViewModel, directly.
namespace rtg.ViewModels
{
    public static class EntityProperties
    {
        public static readonly DependencyProperty EntityProperty = DependencyProperty.RegisterAttached(
        "Entity", typeof(EntityViewModel), typeof(EntityProperties));

        public static void SetEntity(DependencyObject element, EntityViewModel value)
        {
            element.SetValue(EntityProperty, value);
        }

        public static EntityViewModel GetEntity(DependencyObject element)
        {
            return (EntityViewModel)element.GetValue(EntityProperty);
        }
    }
}
My base icon control switches which template it uses based on the presence of certain components. I've tried setting the EntityProperties:Entity property in a million different ways, whether it's directly on the template we switch to (where 'DirectOwnerEntity' is the ship EntityViewModel we're wrapping, else our DataContext must be a ship's EntityViewModel directly):
<UserControl x:Class="rtg.Controls.ObjectIconBase"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:rtg.Controls"
             xmlns:viewModels="clr-namespace:rtg.ViewModels"
             mc:Ignorable="d" HorizontalAlignment="Stretch"
             >
    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
        <Grid.Resources>
            <ControlTemplate x:Key="ShipIconControlTemplate">
                <local:SpaceObjectIcon 
                    viewModels:EntityProperties.Entity="{Binding Path=DataContext, RelativeSource={RelativeSource Self}}">
                </local:SpaceObjectIcon>
            </ControlTemplate>

            <ControlTemplate x:Key="WorldObjectIconTemplate">
                <local:WorldObjectIcon viewModels:EntityProperties.Entity="{Binding DirectOwnerEntity}"></local:WorldObjectIcon>
            </ControlTemplate>

            <!-- Switch to ShipIconControlTemplate if the VM has a valid DirectOwnerEntity-->
            <Style x:Key="ObjectIconTemplateControlStyle" TargetType="Control">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding DirectOwnerEntity, Converter={StaticResource IsNotNullConverter}}" Value="True">
                        <Setter Property="Template" Value="{StaticResource ShipIconControlTemplate}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding DirectOwnerEntity, Converter={StaticResource IsNotNullConverter}}" Value="False">
                        <Setter Property="Template" Value="{StaticResource WorldObjectIconTemplate}"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Grid.Resources>
or inside the triggers' setters:
<!-- Switch to ShipIconControlTemplate if the VM has a valid DirectOwnerEntity-->
<Style x:Key="ObjectIconTemplateControlStyle" TargetType="Control">
	<Style.Triggers>
		<DataTrigger Binding="{Binding DirectOwnerEntity, Converter={StaticResource IsNotNullConverter}}" Value="True">
			<Setter Property="Template" Value="{StaticResource ShipIconControlTemplate}"/>
			<Setter Property="viewModels:EntityProperties.Entity" Value="{Binding Path=DataContext, RelativeSource={RelativeSource Self}}"/>
		</DataTrigger>
		<DataTrigger Binding="{Binding DirectOwnerEntity, Converter={StaticResource IsNotNullConverter}}" Value="False">
			<Setter Property="Template" Value="{StaticResource WorldObjectIconTemplate}"/>
			<Setter Property="viewModels:EntityProperties.Entity" Value="{Binding DirectOwnerEntity}"/>
		</DataTrigger>
	</Style.Triggers>
</Style>
And no matter what, I can't get it to work inside the ObjectIconOverrideTemplate I posted at the start.

Directly binding
    <ControlTemplate x:Key="ObjectIconOverrideTemplate">
        <Grid>
            <Image Source="{Binding (viewModels:EntityProperties.Entity)[IconOverrideComponent].IconName}" Stretch="Uniform" Height="Auto" Width="Auto"/>
            <Rectangle Height="Auto" Width="Auto" Stretch="Uniform" Style="{StaticResource ObjectIconColourStyle}">
                <Rectangle.OpacityMask>
                    <ImageBrush ImageSource="{Binding (viewModels:EntityProperties.Entity)[IconOverrideComponent].IconName}"/>
                </Rectangle.OpacityMask>
            </Rectangle>
        </Grid>
    </ControlTemplate>
Path and/or relative source
    <ControlTemplate x:Key="ObjectIconOverrideTemplate">
        <Grid>
            <Image Source="{Binding RelativeSource={RelativeSource Self}, Path=(viewModels:EntityProperties.Entity)[IconOverrideComponent].IconName}" Stretch="Uniform" Height="Auto" Width="Auto"/>
            <Rectangle Height="Auto" Width="Auto" Stretch="Uniform" Style="{StaticResource ObjectIconColourStyle}">
                <Rectangle.OpacityMask>
                    <ImageBrush ImageSource="{Binding RelativeSource={RelativeSource Self}, Path=(viewModels:EntityProperties.Entity)[IconOverrideComponent].IconName}"/>
                </Rectangle.OpacityMask>
            </Rectangle>
        </Grid>
    </ControlTemplate>
I usually don't get any errors if I use Path, it just silently fails, or if just binding I get something like this, where FleetNodeEntityViewModel is an EntityViewModel subclass, and is the ViewModel underneath
System.Windows.Data Error: 17 : Cannot get 'Entity' value (type 'EntityViewModel') from '' (type 'FleetNodeEntityViewModel'). BindingExpression:Path=(0)[IconOverrideComponent].IconName; DataItem='FleetNodeEntityViewModel' (HashCode=15114764); target element is 'Image' (Name=''); target property is 'Source' (type 'ImageSource') InvalidCastException:'System.InvalidCastException: Unable to cast object of type 'rtg.ViewModels.FleetNodeEntityViewModel' to type 'System.Windows.DependencyObject'.
at MS.Internal.Data.PropertyPathWorker.GetValue(Object item, Int32 level)
at MS.Internal.Data.PropertyPathWorker.RawValue(Int32 k)'
And sometimes a "Cannot find governing FrameworkElement for target element" type of error.

Was looking at this previous suggestion (viewtopic.php?p=15757#p15760) and a StackOverflow post about attached properties (https://stackoverflow.com/questions/303 ... 7#30385037) but can't get it to work.

Any help making this entity-component look-up redirectable in this manner would be appreciated.
 
User avatar
sfernandez
Site Admin
Posts: 3003
Joined: 22 Dec 2011, 19:20

Re: Attached Properties in templates/styles and used in binding?

15 Jan 2024, 11:55

If you expose a property "Target" in your viewmodel that returns the component that you want to bind (the node or the target entity) it will simplify the binding to just:
<Image Source="{Binding Target[IconOverrideComponent].IconName}"/>
Is this what you are trying to do?

Regarding the attached properties remember they can only be used in DependencyObjects, they won't work on your view models. So a binding like the following might not be correct because is trying to look for a DependencyProperty in the DataContext (your view model?):
<Image Source="{Binding (viewModels:EntityProperties.Entity)[IconOverrideComponent].IconName}"/>
 
HateDread
Topic Author
Posts: 72
Joined: 06 Feb 2020, 10:27

Re: Attached Properties in templates/styles and used in binding?

15 Jan 2024, 14:43

Not exactly, I don't think - I'm trying to have these templates/styles be able to re-target to a different path based on something like an Attached Property, but not all of them will do that. So you can imagine if I had a Target property, but only want some of my shared templates/styles to use that, and some to use the ViewModel itself, and those are mixed together, setting a single Target property and using it for everything breaks that. Specifically, the node _is_ an entity as well, and sometimes we want to read components off of it, and sometimes off the target entity, but only these node entities _have_ a target entity, so it gets a bit funky trying to have a single point set at the ViewModel level for all things that might use it. Sometimes Target would be the self/datacontext, sometimes a nested entity, and those are mixed in the same overall control.

An example might be a shield/health bar that is reusable on a side-panel/menu built out of nodes, but also used in-world in an object's clickable world icon - the node might wants that reusable control/template to access TargetEntity[HealthComponent], but an in-world object icon that has no node, only the target entity, will just directly look up the component as [HealthComponent] because its view model is the entity itself. The control will know, based on e.g. DataTrigger, whether it should go one way or the other, so I figured Attached Properties were the fix. Right now I have a copy-pasted version of every case like this so it can go down one path or the other, and that's not great.

To your point - am I not setting my attached properties on a control template, thus on a control and not the view model, like a Canvas.Left or other such attached property examples? Is the issue that I can't use them to talk to/access view models, even if they are set on a control? I'm referring to the "ObjectIconTemplateControlStyle" that swaps the template based on DataTriggers - I figured that meant I was in DependencyObject territory but may be wrong (am new to these!).
 
User avatar
sfernandez
Site Admin
Posts: 3003
Joined: 22 Dec 2011, 19:20

Re: Attached Properties in templates/styles and used in binding?

17 Jan 2024, 10:42

So it is the control that uses the data who knows (through the attached property) if it has to reference the entity directly or the target. Then, if the attached property is set in the control, and assuming the control is the root of the xaml you could have:
<UserControl x:Class="TheNamespace.MyControl"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  x:Name="MyControlRoot">
  ...
  <Image Source="{Binding (viewModels:EntityProperties.Entity)[IconOverrideComponent].IconName, ElementName=MyControlRoot}"/>
  ...
</UserControl>
So the binding points to the control, where the attached property is set. Is this what you are looking for?

Who is online

Users browsing this forum: Ahrefs [Bot] and 4 guests