Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EventTriggerBehavior does not fire bound command in DataTemplate #239

Open
sam-wheat opened this issue Mar 2, 2023 · 10 comments
Open

EventTriggerBehavior does not fire bound command in DataTemplate #239

sam-wheat opened this issue Mar 2, 2023 · 10 comments

Comments

@sam-wheat
Copy link

sam-wheat commented Mar 2, 2023

In the XAML below the Toggled event does not fire the bound PlaceSelectedCommand.

Repo is here. Possibly related to this. Could also be related to this.

    <ItemsControl ui:FrameworkElementExtensions.AncestorType="local:CustomControl1" 
                                      ItemsSource="{TemplateBinding Places}">
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <ToggleSwitch ui:FrameworkElementExtensions.AncestorType="local:CustomControl1"
                                            OnContent="{Binding Name}" OffContent="{Binding Name}" 
                                            IsOn="{Binding IsSelected, Mode=TwoWay}">
                                        <Interactivity:Interaction.Behaviors>
                                            <Interactions:EventTriggerBehavior EventName="Toggled" >
                                                <Interactions:InvokeCommandAction 
                                                    Command="{Binding (ui:FrameworkElementExtensions.Ancestor).PlaceSelectedCommand, 
                                                    RelativeSource={RelativeSource Self},
                                                    Mode=OneWay}"
                                                    CommandParameter="{Binding}">
                                                </Interactions:InvokeCommandAction>
                                            </Interactions:EventTriggerBehavior>
                                        </Interactivity:Interaction.Behaviors>
                                    </ToggleSwitch>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>
@kmgallahan
Copy link

@sam-wheat This issue should be closed as the command does get invoked in the latest version of your repo proj.

@sam-wheat
Copy link
Author

@kmgallahan Thank you very much for your replies.

The code written above is written according to the documentation found here.

At the time I wrote this ticket, my repo reflected this exact code and it does not work. Did I write it incorrectly? Perhaps I am missing something, or perhaps the documentation needs to be updated. In any case, I think this is a fair question as stated and should be closed with a definitive response. It will certainly help me - and hopefully other developers who come across this issue.

The code in my repo does indeed work. After writing this ticked I continued working on the problem and literally by guesswork alone I stumbled across the solution you now see in my repo.

As I requested here the question is now "why does it work?". Did I discover a way of doing it wrong that just happens to work? At the very minimum we have a documentation issue - I can find nothing from MS that suggests the approach I've taken is valid or even supported. In fact I think a good case can be made that my approach is completely wrong, and that AncestorType or ElementName should work fine on their own and that combining them is redundant.

@kmgallahan
Copy link

kmgallahan commented Mar 19, 2023

@sam-wheat

Did I write it incorrectly?

I did not notice that the code in the repo was different - above you use RelativeSource={RelativeSource Self} and in the repo ElementName=layoutRoot.

I'm not entirely sure why RelativeSource Self doesn't work. It is unlikely to be a problem with code in this repo (more likely a XAML compilation issue).

As I requested microsoft/microsoft-ui-xaml#8274 the question is now "why does it work?".
Did I discover a way of doing it wrong that just happens to work?

I replied there before my above comment. As mentioned there, you are running into some preexisting Binding issues.

Again, I'm not sure what exact syntax you are referring to when mentioning AncestorType or ElementName alone.


Just as a note, you appear to be creating a Control, whereas I would be creating a UserControl instead.

A Control uses a .cs file and Style in a ResourceDictionary, while a UserControl uses a .xaml file with .cs file code behind. The former is generally for creating reusable controls that could go into a UI library, for example. The latter is for building view parts/components for an application (where you would be directly making references to business logic in command implementations).

@sam-wheat
Copy link
Author

sam-wheat commented Mar 19, 2023

Again, I'm not sure what exact syntax you are referring to when mentioning AncestorType or ElementName alone.

Very sorry for the confusion. I posted this issue and then kept working on it. I found a way to make it work - but I don't understand why - hence this post. For clarity, the working code is below with comments on relevant lines.

                <Border ui:FrameworkElementExtensions.AncestorType="local:CustomControl1" x:Name="layoutRoot"
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">

					// Omitted...

					<ItemsControl ItemsSource="{TemplateBinding Places}">
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <ToggleSwitch 
                                            OnContent="{Binding Name}" OffContent="{Binding Name}" 
                                            IsOn="{Binding IsSelected, Mode=TwoWay}">
                                        <Interactivity:Interaction.Behaviors>
                                            <Interactions:EventTriggerBehavior EventName="Toggled" >
                                                <Interactions:InvokeCommandAction 
                                                    Command="{Binding (ui:FrameworkElementExtensions.Ancestor).PlaceSelectedCommand, // Ancestor required here
                                                    ElementName=layoutRoot, Mode=TwoWay}"			 //	ElementName required here (redundant)
                                                    CommandParameter="{Binding}">
                                                </Interactions:InvokeCommandAction>
                                            </Interactions:EventTriggerBehavior>
                                        </Interactivity:Interaction.Behaviors>
                                    </ToggleSwitch>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>
                    </StackPanel>
                </Border>

Also - I am creating a reusable (templated) control. Others have suggested I use x:Bind and a use a resouce dictionary with code behind but I know of no (working) way to do that.

@kmgallahan
Copy link

Command="{Binding (ui:FrameworkElementExtensions.Ancestor).PlaceSelectedCommand,  // Ancestor required here
ElementName=layoutRoot, Mode=TwoWay}"   //	ElementName required here (redundant)

This is using Binding's Path & ElementName properties together and is equivalent to this 'raw' C#:

layoutRoot.Ancestor.PlaceSelectedCommand

Where you attached the Ancestor property to <Border x:Name="layoutRoot"> when you called ui:FrameworkElementExtensions.AncestorType=...

If you omitted ElementName, then you'd have to specify a RelevantSource or Source. If you didn't use either then on creating the Binding the compiler wouldn't know what object to use the Path on:

?????.Ancestor.PlaceSelectedCommand

Similarly, if you omitted the Path part:

layoutRoot.?????

If you are asking why (ui:FrameworkElementExtensions.Ancestor).PlaceSelectedCommand is used instead of just PlaceSelectedCommand:

layoutRoot.?????.PlaceSelectedCommand

@sam-wheat
Copy link
Author

sam-wheat commented Mar 19, 2023

If you omitted ElementName, then you'd have to specify a RelevantSource or Source.

True - in fact RelativeSource={RelativeSource Self} is the only documented syntax. That said, it does not work for the example cited in this issue.

As far as the working code, I imagine this XAML:

	Command="{Binding (ui:FrameworkElementExtensions.Ancestor).PlaceSelectedCommand

becomes something like this at run time because Ancestor is inherited from Border:

	Command="{Binding local:CustomControl1.PlaceSelectedCommand

Why would ElementName would be required for Ancestor? All the info the binding needs can be resolved.
Likewise, ElementName works fine on it's own by traversing the tree - no need for Ancestor to be set.

IDK why RelativeSource={RelativeSource Self} is required for Ancestor binding - but it is the documented usage so I'll roll with it.

If anything, using Ancestor and ElementName together should throw an error - It is telling the XAML parser to take two different pathways to resolve some higher level element.

@kmgallahan
Copy link

kmgallahan commented Mar 20, 2023

in fact RelativeSource={RelativeSource Self} is the only documented syntax

To do only what and referencing what documentation?

Why would ElementName would be required for Ancestor?

Binding defaults its source object the to current DataContext. The DataContext in your code at the top is automatically set to the data object being templated inside your DataTemplate. It is why you can just use {Binding Name}. This also explains why RelativeSource=Self isn't working, because self = the data object (a PlaceModel).

If you weren't inside a DataTemplate then the ElementName could be omitted (depending on where Ancestor was attached).

@sam-wheat
Copy link
Author

To do only what and referencing what documentation?

Also referenced above

There is no inheritance.

Please see this.

This also explains why RelativeSource=Self isn't working, because self = the data object (a PlaceModel).

Agree it seems wrong but as I mentioned in my last comment is the documented syntax.

If you weren't inside a DataTemplate then the ElementName could be omitted (depending on where Ancestor was attached).

I don't see how that makes a difference. Either should work on their own IMO. Using them together in any circumstance is at best redundant and possibly contradictory.

@kmgallahan
Copy link

kmgallahan commented Mar 21, 2023

Also referenced above
documented syntax.

The problems you are running into involve data binding, which isn't the focus of the FrameworkElement Extensions API docs. I would review:

Data binding in depth

{x:Bind} markup extension

{Binding} markup extension

Please see this.

As per the title of the linked page: Property value inheritance (WPF .NET)

That is for WPF, and you are not working with WPF. This is WASDK/WinUI 3, which uses essentially the same XAML functionality as UWP. There is no such thing here: https://stackoverflow.com/questions/43424845/dependencyproperty-value-inheritance-equivalent-in-uwp

I don't see how that makes a difference.

Again, you are inside a DataTemplate, so the default binding source object (that which is used when omitting ElementName/RelativeSource/Source) for Binding is the current data object.

If you manually set x:Name="tt" DataContext="tt" on your ToggleSwitch you'll see you can omit ElementName, but now the other bindings without it are broken, like {Binding Name}.

@sam-wheat
Copy link
Author

Got it, thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants