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

"Visual Bridge" for items outside the visual tree #352

Closed
ShawnWeaver opened this Issue Jul 26, 2016 · 4 comments

Comments

Projects
None yet
3 participants
@ShawnWeaver

I've been using Caliburn for a while now, and one problem that still trips me up is working with controls that aren't part of the main visual tree: context menus, popups, etc. I've avoided these controls as much as possible because of the pain they present.

I've been doing some searching for how to solve the popup issue because I'd like to use WPF ToolBars in my current application (the automatic overflow feature is extremely handy). However, I have a hierarchy that makes it difficult to target the proper method since I rely on Caliburn's action bubbling to find the right context. I haven't been able to find a solution that preserves the visual tree bubbling; most solutions simply set the action's target, but I need the target to be the screen and the tool bar is in a data template several layers away from the screen with no reference to the screen that hosts it.

To solve my problem, I created a simple attached property for a "visual bridge" - a way for a control to specify a "visual parent" directly, and I replaced Caliburn's SetMethodBinding action with a slightly modified copy. The end result is easy to use and seems pretty flexible:

<ToolBar x:Name="RootToolBar">
    <Button Content="Go"
            ToolBar.OverflowMode="Always"
            cal:Message.Attach="Go"
            Behaviors:VisualBridge.Parent="{Binding ElementName=RootToolBar}"/>
</ToolBar>

The only change to the SetMethodBinding action is a check for the attached property

//formerly: currentElement = VisualTreeHelper.GetParent(currentElement);
currentElement = VisualBridge.HasParentSet(currentElement)
                 ? VisualBridge.GetParent(currentElement)
                 : VisualTreeHelper.GetParent(currentElement);

With that said, I'm wondering if this would be a useful addition to the Caliburn project, or if there's a different method for handling these situations that would be preferred. I'm unsure if this would work on more than the standard .NET WPF platform.

If this is a worthwhile addition, I'd be happy to make a pull request, though I've never done that and I might have to fumble my way through it... (And there may be a better name than "VisualBridge".)

@nigel-sampson

This comment has been minimized.

Show comment
Hide comment
@nigel-sampson

nigel-sampson Jul 27, 2016

Contributor

Interesting, the whole "things not in the visual tree" is something most people get caught up on.

Will have a think about what it would mean to include it in the project, ideally it should work everywhere. One way may be to add an extension point so that behavior can be customised and usage of something like VisualBridge can be wired in.

Contributor

nigel-sampson commented Jul 27, 2016

Interesting, the whole "things not in the visual tree" is something most people get caught up on.

Will have a think about what it would mean to include it in the project, ideally it should work everywhere. One way may be to add an extension point so that behavior can be customised and usage of something like VisualBridge can be wired in.

@ShawnWeaver

This comment has been minimized.

Show comment
Hide comment
@ShawnWeaver

ShawnWeaver Jul 27, 2016

Being able to adjust how Caliburn bubbles up the visual tree could be just as effective. In this case, some way to tell Caliburn how to get an element's parent, with the default method being VisualTreeHelper.GetParent. It would certainly be easier to reuse in other places where the VisualTreeHelper is used.

Being able to adjust how Caliburn bubbles up the visual tree could be just as effective. In this case, some way to tell Caliburn how to get an element's parent, with the default method being VisualTreeHelper.GetParent. It would certainly be easier to reuse in other places where the VisualTreeHelper is used.

@nigel-sampson nigel-sampson added this to the v3.1.0 milestone Aug 8, 2016

@drventure

This comment has been minimized.

Show comment
Hide comment
@drventure

drventure Dec 7, 2016

I've been bit pretty hard by this issue as well.
I could be way off, but the basic problem appears to be those controls that don't participate in the VisualTree. and those controls all seem to have a "PlacementTarget". Hence so many solutions to this problem seem to center around sticking the DataContext of the parent into the Tag property of the PlacementTarget.

I had some time today so I decided to dig into this.

Turns out, I was able to intercept the SetMethodBinding via the normal channels and "extend" it's functionallity a little to accomodate those cases where No action target was found as a result of this whole VisualTree thing.

Have a look

//First, have a place to save the existing default SetMethodBinding function
static Action<ActionExecutionContext> _base_setMethodBinding;
....
//at some point during initializing Caliburn, save the existing function
_base_setMethodBinding = ActionMessage.SetMethodBinding;
//and replace it with our override 
ActionMessage.SetMethodBinding = SetMethodBinding;
....
//in a static class somewhere, define our override
internal static void SetMethodBinding(ActionExecutionContext context)
	{
		//try the normal method
		_base_setMethodBinding(context);
		//it worked, so we're done
		if (context.View != null) return;

		var source = context.Source;

                    //This loop essentially looks for the first parent in the hierarchy to have a PlacementTarget
		FrameworkElement currentElement = source;
		while (currentElement != null)
		{
			var ptProp = currentElement.GetType().GetProperty("PlacementTarget");
			if (ptProp != null)
			{
				var pt = ptProp.GetValue(currentElement) as FrameworkElement;
				if (pt != null)
				{
					context.Source = pt;
					_base_setMethodBinding(context);
					return;
				}
			}
			currentElement = currentElement.Parent as FrameworkElement;
		}
		return;
	}

I'm reusing all the base SetMethodCode, just attempting the search first from the initial control source, and if that doesn't work, look for a PlacementTarget, and if I find one, continue the search for a valid target at that point.

I'm sure there's edge cases to contend with, but so far, this code seems to "just work", and with 0 impact on existing Caliburn.

drventure commented Dec 7, 2016

I've been bit pretty hard by this issue as well.
I could be way off, but the basic problem appears to be those controls that don't participate in the VisualTree. and those controls all seem to have a "PlacementTarget". Hence so many solutions to this problem seem to center around sticking the DataContext of the parent into the Tag property of the PlacementTarget.

I had some time today so I decided to dig into this.

Turns out, I was able to intercept the SetMethodBinding via the normal channels and "extend" it's functionallity a little to accomodate those cases where No action target was found as a result of this whole VisualTree thing.

Have a look

//First, have a place to save the existing default SetMethodBinding function
static Action<ActionExecutionContext> _base_setMethodBinding;
....
//at some point during initializing Caliburn, save the existing function
_base_setMethodBinding = ActionMessage.SetMethodBinding;
//and replace it with our override 
ActionMessage.SetMethodBinding = SetMethodBinding;
....
//in a static class somewhere, define our override
internal static void SetMethodBinding(ActionExecutionContext context)
	{
		//try the normal method
		_base_setMethodBinding(context);
		//it worked, so we're done
		if (context.View != null) return;

		var source = context.Source;

                    //This loop essentially looks for the first parent in the hierarchy to have a PlacementTarget
		FrameworkElement currentElement = source;
		while (currentElement != null)
		{
			var ptProp = currentElement.GetType().GetProperty("PlacementTarget");
			if (ptProp != null)
			{
				var pt = ptProp.GetValue(currentElement) as FrameworkElement;
				if (pt != null)
				{
					context.Source = pt;
					_base_setMethodBinding(context);
					return;
				}
			}
			currentElement = currentElement.Parent as FrameworkElement;
		}
		return;
	}

I'm reusing all the base SetMethodCode, just attempting the search first from the initial control source, and if that doesn't work, look for a PlacementTarget, and if I find one, continue the search for a valid target at that point.

I'm sure there's edge cases to contend with, but so far, this code seems to "just work", and with 0 impact on existing Caliburn.

@nigel-sampson

This comment has been minimized.

Show comment
Hide comment
@nigel-sampson

nigel-sampson May 2, 2017

Contributor

I've added an extension point BindingScope.GetVisualParent that is currently implemented by VisualTreeHelper.GetParent.

This'll allow extension using something like VisualBridge or PlacementTarget.

Contributor

nigel-sampson commented May 2, 2017

I've added an extension point BindingScope.GetVisualParent that is currently implemented by VisualTreeHelper.GetParent.

This'll allow extension using something like VisualBridge or PlacementTarget.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment