Possible Bug in Attached Action Message Parser

Jan 27, 2011 at 8:21 PM
Edited Jan 27, 2011 at 8:30 PM

Using changeset e50e3a4d25d9 dated 1/26/2011 5:59 AM

The following doesn't pass the ActionPointsFilterTextBox.Text correctly (it's null):

 

<SearchTextBox:SearchTextBox 
     x:Name="ActionPointsFilterTextBox"
     cal:Message.Attach="[Event OnSearch] = [Action FilterActionsPoints($datacontext, ActionPointsFilterTextBox.Text)];
                         [Event OnClearSearch] = [Action ClearActionsPointsFilter($datacontext)]"/>

 

However, the following syntax works:

 

<i:Interaction.Triggers>
    <i:EventTrigger EventName="OnSearch">
        <cal:ActionMessage MethodName="FilterActionsPoints">
            <cal:Parameter Value="{Binding}" />
            <cal:Parameter Value="{Binding Path=Text, ElementName=ActionPointsFilterTextBox}" />
        </cal:ActionMessage>
    </i:EventTrigger>
    
    <i:EventTrigger EventName="OnClearSearch">
        <cal:ActionMessage MethodName="ClearActionsPointsFilter">
            <cal:Parameter Value="{Binding}" />                                                                        
        </cal:ActionMessage>
    </i:EventTrigger>
</i:Interaction.Triggers>

 

Jan 27, 2011 at 9:48 PM

Joe, I tried to reproduce your issue, but using this implementation of SearchTextBox I had no issues.

The only reason the compact syntax could fail is your SearchTextBox not being a FrameworkElement, otherwise it should work as expected.

Can you provide a small repro project?

Jan 27, 2011 at 9:50 PM

Thanks for looking into this, I'll get right on it.

Jan 27, 2011 at 11:01 PM
Edited Jan 27, 2011 at 11:07 PM

Heh, pretty close find on that SearchTextBox.  The SearchTextBox I'm using I wrote myself, it's a cross platform WPF/Silverlight TextBox that's been much refactored/improved version of that WPF SearchTextBox. I've included it in the sample, which you can download from here:

http://www.bitspy.com/CBAttachedActionMessageParserBug.zip

In creating the example I found the real scenario that's needed to re-create the issue. It appears if you are using the attached syntax in a items collection where the x name of the target FrameWorkElement will be repeated per item it only reads from the first instance created (the first item's textbox). However the Blend Trigger syntax works fine in an items collection. The above example will illustrate the differences. Just type in the text boxes and examine the output in the listbox at the bottom.

Coordinator
Jan 28, 2011 at 12:01 AM
Edited Jan 28, 2011 at 12:01 AM

Lucky for you, I think I already fixed that bug. 

404dd9212bef 
by EisenbergEffect
Jan 12 at 
12:23 PM

I believe it's the same issue. I tried your sample with the latest code and everything seams to work fine. Try updating and see if it works for you.

Jan 28, 2011 at 12:09 AM

I just tested it out, and I can confirm that Rob is (as usual) right. ;)

 

Jan 28, 2011 at 12:17 AM

Something's weird that sample should be packaged with what should be the build from yesterday, which is newer than Jan 12th, checking...

Jan 28, 2011 at 1:02 AM
Edited Jan 28, 2011 at 1:13 AM

I rebuilt with today's build of Caliburn Micro, the issue is still there.

When you run the app there should be 3 items, each item has 2 text boxes. The first textbox calls an action via an attached action message. The second textbox calls an action via a blend behavior.

1.) Type "abc" into the first textbox in the first item. Status line will show the filter text value is "abc".

2.) type "123" into the first textbox in the second item. Status line will show the filter text value is "abc". The value should be "123". This is the incorrect behavior.

 

3.) Type "abc" into the second textbox in the first item. Status line will show the filter text value is "abc".

4.) Type "123" into the second textbox in the second item. Status line will show the filter text value is "123". As it should. 

 

The first text box in each item uses an attached message. The second text box in each item used a blend behavior. The blend behavior sends the right message from the right textbox, the attached action message does not (for items 2 and 3 it sends the text from the textbox in item 1).

Coordinator
Jan 28, 2011 at 1:06 AM

I'm pretty sure it worked for me. I'll check it again in the morning.

Coordinator
Jan 28, 2011 at 2:27 AM

Ok. Confirmed the bug. This is going to be a pain, I know. Give me some time...

Jan 28, 2011 at 2:51 AM
Edited Jan 29, 2011 at 1:38 AM

I was too quick answering. Indeed the issue is there, but it was not the one I was expecting, since in the first post you stated that the parameter was null... while I was getting a proper value.

The last post, where you cleanly state the issue, has indeed helped me a lot to understand what's going on. :)

 

And to add something nice to the news, I found what is the problem. :)

The fact is that your visual tree has multiple objects with the same name and the extension function used to retrieve the named objects (ExtensionsMethods.GetNamedElementsInScope), traverses the tree top-down.

To be fair, the function will drill-up the tree until it finds either a named element scope (setted by the Bind.Model), a UserControl (that is considered the closure of a named scope) or a tree root (having a null parent), and than starts returning the children in a breadth-first way.

Unfortunally the SearchTextBox does not meet either of the above criteria, so the search is performed up to the root tree every time (3 times).

To be clearer consider the following schema as a representation of your visual tree:

Root

|_ ...

|_ StackPanel

   |_ SearchTextBox (ActionPointsFilterTextBox) [A]

   |_ SearchTextBox (ActionPointsFilterTextBox) [B]

   |_ SearchTextBox (ActionPointsFilterTextBox) [C]

(bear with me for the poor ASCII art --')

As you can imagine, this is what's occurring:

  • When ExtensionsMethods.GetNamedElementsInScope is called on [A], just one element with the required name is returned
  • When it is called on [B], two are returned
  • When it is called on [C], all of them are returned

Now, since the named element is the first having the specified name (because of the search performed by ExtensionMethods.FindName), your parameters are always bound to the first SearchTextBox.

 

There are some ways to fix this:

  1. Return the last element in the collection of named objects matching the criteria instead of using ExtensionMethods.FindName
  2. Traverse the visual tree bottom-up, so that the nearest named element is always found first (the effect is exactly the obove one, but you can implement this strategy replacing ExtensionMethods.GetNamedElementsInScope and is probably the best solution for the CM framework too, since I suppose is the same strategy used by the framework name resolution... well, simplified)
  3. Mark every SearchTextBox as a named element scope setting the View.IsScopeRootProperty to true (either by code or modifying source to add getter and setter, otherwise the compiler complains)
  4. Determine the realization of a DataTemplate as a name scope (not a good idea, since named object outside templates would not be found)

I suppose that solution 2 is the way to go, both to avoid the issue in your code, and as a proposed fix for the CM framework.

@Rob: I hope I avoided you some headaches!

 

Edit:

I edited my proposed solution, since I figured out that I was thinking in a too simplicistic way.

The fact is that the search should be probably performed on a nearest-neighbour basis (i.e. count the distance of each named element from the start point and sort the list accordingly). Maybe it could be worth to check the algorithm that WPF/Silverlight use internally, to try to match the deafult naming resolution strategy.

@Rob: Maybe I gave you even MORE headaches!

Jan 28, 2011 at 10:38 AM

Uhmmm... I just found a simple solution, but I don't know if it was already discarded due to some drawback I am not aware of.

Instead of enumerating named elements manually, it is possible to just use the FrameworkElement.FindName(DependencyObject obj) function to retrieve the object by the given name, in the current name scope

 

        /// <summary>
        /// Creates a binding on a <see cref="Parameter"/>.
        /// </summary>
        /// <param name="target">The target to which the message is applied.</param>
        /// <param name="parameter">The parameter object.</param>
        /// <param name="elementName">The name of the element to bind to.</param>
        /// <param name="path">The path of the element to bind to.</param>
        /// <param name="bindingMode">The binding mode to use.</param>
        public static void BindParameter(FrameworkElement target, Parameter parameter, string elementName, string path, BindingMode bindingMode)
        {
            var element = elementName == "$this"
                ? target
                : target.FindName(elementName) as DependencyObject;
            if (element == null)
                return;

            if (string.IsNullOrEmpty(path))
                path = ConventionManager.GetElementConvention(element.GetType()).ParameterProperty;

            var binding = new Binding(path)
            {
                Source = element,
                Mode = bindingMode
            };

#if SILVERLIGHT
            var expression = (BindingExpression)BindingOperations.SetBinding(parameter, Parameter.ValueProperty, binding);

            var field = element.GetType().GetField(path + "Property", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
            if (field == null)
                return;

            ConventionManager.ApplySilverlightTriggers(element, (DependencyProperty)field.GetValue(null), x => expression);
#else
            binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            BindingOperations.SetBinding(parameter, Parameter.ValueProperty, binding);
#endif
        }

The nice aspect of this solution is that the result should be consistent with the one provided by a binding having ElementName set instead of Source.

I tested this solution with the repro project, and everything works fine. I even checked if the functionality is supported in either WPF/Silverlight/WP7, and it does (at least for .NET 4.0).

Is there any drawback that prevents the usage of this method?

 

 

Coordinator
Jan 28, 2011 at 1:11 PM

It cannot be used because it is case sensitive and we need case in-sensitivity.

Coordinator
Jan 28, 2011 at 2:54 PM

Fixed as of revision d22d06c4c4f0 Also, I decided this code needed to be in its own class, so I moved the relevant methods to a new class called BindingScope.

Jan 28, 2011 at 9:04 PM

Just got a chance to test it out, works great, thanks for all the hard work!

Coordinator
Jan 29, 2011 at 12:18 AM

That was a tricky one ;)