How to prevent a ScrollViewer from handling MouseMove event of a descendant? (Silverlight for Windows Phone) - silverlight

Consider this:
<ScrollViewer>
<!-- Several Controls /-->
<MyControl MouseMove="myMouseMoveHandler" />
<!-- Several Controls /-->
</ScrollViewer>
MyControl is a HSV color selection control with the color spectrum on a circle which can rotate, and the nuances of the selected hue on a triangle. It looks awesome, but sadly I cannot post a picture yet (rep). It really needs to be able to handle mouse movement in all directions on its surface.
Now when I move the mouse on MyControl (and it correctly handles the movement), the ScrollViewer still scrolls!
This happens even when it is the only control in the ScrollViewer, the movement starts and ends inside my control, and/or I set e.Handled = true in both the MouseLeftButtonDown / -Up events. Using CaptureMouse() in ..Down and ReleaseMouseCapture() in ..Up doesn't help either.
You will agree that I cannot change the ScrollViewer implementation (or can I?), and I cannot guarantee that my control is never hosted inside a ScrollViewer (eg. because I want to publish it).
It must be possible to prevent the ScrollViewer from getting the MouseMove. Proof: simply replace MyControl with a ListBox containing more items than fit into its Height, and you can swipe through the ListBox items without the ScrollViewer reacting.
But how? Is it also a ScrollViewer inside the ListBox and that's why it works there, or can it be done for my control too?

Alright, I found a solution that works nicely.
My thoughts were so fixed to e.Handled (unavailable in MouseMove), IsHitTestVisible (hides all children from touch events as well) and sorts of stuff, I didn't see the obvious.
Here is the code in case somebody has the same question:
struct ScrollVisibilities
{
public ScrollBarVisibility Horizontal;
public ScrollBarVisibility Vertical;
}
Dictionary<ScrollViewer, ScrollVisibilities> scrollersStates = new Dictionary<ScrollViewer, ScrollVisibilities>();
bool scrollersDisabled;
void disableScrollViewers(bool disable)
{
if (scrollersDisabled == disable) // can't disable if disabled or enable if enabled
return;
scrollersDisabled = disable;
if (disable)
{
DependencyObject dpo = Parent;
while (dpo is FrameworkElement)
{
if (dpo is ScrollViewer)
{
ScrollViewer s = dpo as ScrollViewer;
ScrollVisibilities v = new ScrollVisibilities()
{
Horizontal = s.HorizontalScrollBarVisibility,
Vertical = s.VerticalScrollBarVisibility
};
scrollersStates.Add(s, v);
s.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
s.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
}
dpo = ((FrameworkElement)dpo).Parent;
}
}
else // restore
{
foreach (ScrollViewer s in scrollersStates.Keys)
{
s.HorizontalScrollBarVisibility = scrollersStates[s].Horizontal;
s.VerticalScrollBarVisibility = scrollersStates[s].Vertical;
}
scrollersStates.Clear();
}
}
In MouseLeftButtonDown, I disableScrollViewers(true), and hook into Touch.FrameReported.
In Touch_FrameReported, I disableScrollViewers(false) when all touch points have Action == Up. That way, I get the Up event even when it happens outsided MyControl.
There are limitations to this approach, as disabling the ScrollViewers will cause them to jump to their (and their childrens) unscrolled state. So I put MyControl on the top and set all the alignments accordingly.

Related

WPF Adorner appear on mouseover animation - what's the best pattern?

An Adorner is defined over part of an Image. The required behavior is as follows:
When the mouse is over the image area, including the Adorner area, the Adorner appears.
When the mouse leaves the image and Adorner area, the Adorner dissapears.
Adorner appearing and disappearing is to be through a fade in / out animation, accordingly.
A click on the Adorner area must raise event AdornerClicked
A click on the area over the Image which is not hidden by the adorner, must rais ImageClicked.
A naive implementation
Attach an animation on the Adorner opacity on the Image's MouseEnter and MouseLeave events, and attach Click events for each. This however causes the Adorner to disappear when the mouse is directly above it (as a MouseLeave is triggered on the Image below), violating requirement number 1.
A possible amendment to the naive implementation is to set IsHitTestVisible=false on the Adorner. However, no clicks are then captured by the Adorner, violating requirement number 4.
What is the correct pattern which will fulfill the requirements?
A bit old question but I've just had the same problem and could not find an answer so here's what I've come up with.
So the problem is that the control and its adorner are overlapping and setting the adorner to visible triggers a MouseLeave on the adorned control because it is now covered by the adorner.
The solution is to react on every MouseEnter and MouseLeave on both the adorned control and its adorner(s) and do a hit test manually. If any of them are hit then the adorner(s) should be visible otherwise collapsed.
So you need to be able to get the adorners from the adorned control and vica versa. Getting the adorned control from the adorner is no problem (use the AdornedElement property) but getting the adorners for a control is not provided by the framework (AFAIK) so I use a dictionary that maps my controls to a list of their adorners.
Here's the code inside my Panel-derived class (that contains and arranges my controls and their adorners):
private readonly Dictionary<Control, List<Adorner>> _controlToAdornersMap;
...
private void CreateMyControl()
{
var control = new MyControl();
control.MouseEnter += OnMyControlMouseEnterOrLeave;
control.MouseLeave += OnMyControlMouseEnterOrLeave;
Children.Add(control);
AddAdorners(control);
}
private void AddAdorners(Control control)
{
var myAdorner = new MyAdorner(control);
myAdorner.MouseEnter += OnMyAdornerMouseEnterOrLeave;
myAdorner.MouseLeave += OnMyAdornerMouseEnterOrLeave;
var adornerLayer = AdornerLayer.GetAdornerLayer(control);
adornerLayer.Add(myAdorner);
_controlToAdornersMap[control] = new List<Adorner> {myAdorner};
}
private void OnMyControlMouseEnterOrLeave(object sender, MouseEventArgs e)
{
HitTestAndSetAdornersVisibility((MyControl)sender, e);
}
private void OnMyAdornerMouseEnterOrLeave(object sender, MouseEventArgs e)
{
var adorner = (Adorner)sender;
HitTestAndSetAdornersVisibility((MyControl)adorner.AdornedElement, e);
}
private void HitTestAndSetAdornersVisibility(MyControl control, MouseEventArgs e)
{
var adorners = _controlToAdornersMap[control];
var hitTestSubjects = new List<UIElement> { control }.Concat(adorners);
var hit = hitTestSubjects.Any(i => VisualTreeHelper.HitTest(i, e.GetPosition(i)) != null);
SetAdornersVisibility(adorners, hit ? Visibility.Visible : Visibility.Collapsed);
}
private static void SetAdornersVisibility(IEnumerable<Adorner> adorners, Visibility visibility)
{
if (adorners != null)
foreach (var adorner in adorners)
adorner.Visibility = visibility;
}

Popup to appear on the bottom-right corner of its parent

I'm trying to design a Popup which will appear on the bottom-right corner of its PlacementTarget
Let's admit that you set its PlacementTarget to a Window, well, the Popup will act as classic toaster notifications.
Given the fact that WPF is not smart enough to provide us a "corner" solution, I'm trying to implement a new control, inheriting from Popup , which will place itself at the appropriate location.
Here is my first idea: work on Loaded event to determine where should I place the Popup.
Problem? I don't want to give any fixed dimensions to the popup, which is supposed to size itself according to the text displayed.
However, I can't get the ActualWidth property when Loaded event is raised.
I can't have it either when Opened event is raised.
Here is the draft code so far:
public class ExceptionPopup : Popup
{
public ExceptionPopup()
{
InitializeComponent();
Loaded += new RoutedEventHandler(ExceptionPopup_Loaded);
}
void ExceptionPopup_Loaded(object sender, RoutedEventArgs e)
{
if (PlacementTarget != null)
{
if (PlacementTarget is FrameworkElement)
{
parentWidth = (PlacementTarget as FrameworkElement).ActualWidth;
parentHeight = (PlacementTarget as FrameworkElement).ActualHeight;
}
}
}
protected override void OnOpened(EventArgs e)
{
this.HorizontalOffset = parentWidth;
this.VerticalOffset = parentHeight;
base.OnOpened(e);
}
}
Is there any other event I could use to catch what I want here?
I'd basically like to set HorizontalOffset to parentWidth - ActualWidth/2 , same for height :)
Any idea?
Thanks!
Usually I set the PlacementTarget to either Bottom or Right, then apply a RenderTransform which shifts the Popup by the remaining value.
For example, I might use Placement=Bottom, then use a RenderTransform to shift the popup (Window.Width - Popup.Width) to the right, and Popup.Height upwards. You might not even need to re-adjust based on the Popup Height/Width becauase MSDN says that Popups are not allowed to be displayed off screen, and it will automatically adjust their placement to keep them visible
Be sure you use a RenderTransform instead of a LayoutTransform, because RenderTransforms get applied after the Popup gets Rendered, so the ActualHeight and ActualWidth will be greater than 0.

Prevent a scrollviewer from refocusing to the left after expanding a child in a treeview

I have a customized TreeView inside a ScrollViewer (I needed to overwrite the template for this application).
The treeviewitems have two columns. One for the togglebutton and the other for the content. The toggle button column width has been set to 0 to hide the toggle button and align all the elements. The items data template contains a canvas that draws a rectangle for each item, which can be quite long in the horizontal, and has a left offset from the canvas.
It all draws correctly but when you double click a rectangle to expand the child, the scrollviewer refocuses the treeview to the left. I assume it is refocusing because it realigns to the toggle button or the left of the item content.
Does anyone have any ideas how to change this behaviour?
Well the suggestion I received from a colleague was to catch the event.
I tried a few different events and found the one that bubbled up to the ScrollViewer.
I caught the MouseDown event on the rectangle, marked it as handled and traversed up the event.Source.Parent until I got to the control to expand the child.
Hope this helps others.
private void GanttBarMouseDown(object sender, MouseButtonEventArgs e)
{
var theVm = ((GanttItemViewModel) ((ContentPresenter) ((FrameworkElement) ((FrameworkElement) e.Source).Parent).TemplatedParent).Content);
if (e.ClickCount == 1)
{
theVm.IsSelected = true;
e.Handled = true;
}
else
{
theVm.IsSelected = true;
theVm.IsExpanded = theVm.IsExpanded ? false : true;
e.Handled = true;
}
}

Wpf Adorner not responding to interactions

I'm trying to create an overlay in wpf (with darkening background), similar to the ones you can find on the web to popup images.
I would like it to be reusable in more than 1 part of the application, with diffent types of content.
this is the temporary code of the constructor of the adorner class (just to try)
private readonly Grid _grid = new Grid();
public DarkOverlayAdorner(UIElement adornedElement, Object content) :
base(adornedElement)
{
_grid.Background = new SolidColorBrush(Color.FromArgb(99, 0, 0, 0));
IsHitTestVisible = true;
var visual = content as UIElement;
if (visual != null)
_grid.Children.Add(visual);
}
In addition in the class (of course), I have the ovverrides of MeasureOverride and ArrangeOverride to give the adorner the correct size of the adorned element, GetVisualChild, and VisualChildCount...
The problem here is that the adorner is correctly shown, but no events or behaviour are applied on the adorned element. For example:
AdornerLayer layer = AdornerLayer.GetAdornerLayer(textBoxProva);
layer.Add(new DarkOverlayAdorner(textBoxProva, new Button{Content = "prova"}));
The button here is shown, but I can-t click the button and no effects on button mouseover are applied.
I still can't figure out the problem.
Ok, I've lost a lot of time trying to figure out what was the problem.
In the end I found the solution:
If you want the element added to react to events, I think that the element must be bound to the visual tree of the adorner.
The way to do it is to use a VisualCollection, intitialized to the adorner itself:
VisualCollection visualChildren;
FrameworkElement #object;
public DarkOverlayAdorner(UIElement adornedElement) :
base(adornedElement)
{
visualChildren = new VisualCollection(this);
#object = new Button {Content = "prova"};
visualChildren.Add(#object);
}
protected override Visual GetVisualChild(int index)
{
return visualChildren[index];
}
This way the events are correctly routed.
You might want to take a look at the ChildWindow control in the Extended WPF Toolkit. It is a control that pops up a Window with a modal background effect, and you can specify the content to put inside the Window.

No events passed to WPF adorner layer

I am trying to make a nice "drag and drop zone" in WPF that is displayed in the adorner layer when something is being dragged into the main application. The problem is that I do not get any events from my adorner, even though it according to documentation should receive all input events since it is in a higher z-order.
To debug my problem I created a really simple example where I have a user control with only a button in it. This user control is displayed in the adorner layer, but I cannot click the button. Why? What have I done wrong?
My adorner class is constructed like this:
public ShellOverlayAdorner(UIElement element, AdornerLayer adornerLayer)
:base(element)
{
_adornerLayer = adornerLayer;
_overlayView = new AdornedElement();
_overlayView.AllowDrop = true;
_adornerLayer.Add(this);
}
and is created in the main window by
private void Window_Loaded(object sender, RoutedEventArgs e)
{
adornerLayer = AdornerLayer.GetAdornerLayer(MyTopGridWithButtonInIt);
ShellOverlayAdorner shell = new ShellOverlayAdorner(MyTopGridWithButtonInIt, adornerLayer);
}
I do not get any events at all from my control, i.e. no mouse clicks, mouse over, button clicks. I cannot even click the button in the adorner layer. What have I done wrong?
I don't know if you already tried that:
If you want the element added to react to events, I think that the element must be bound to the visual tree of the adorner.
The way to do it is to use a VisualCollection, intitialized to the adorner itself, or at least, this way it seems to be working:
VisualCollection visualChildren;
FrameworkElement #object;
public CustomAdorner(UIElement adornedElement) :
base(adornedElement)
{
visualChildren = new VisualCollection(this);
#object = new Button {Content = "prova"};
visualChildren.Add(#object);
}
protected override Visual GetVisualChild(int index)
{
return visualChildren[index];
}
This way the events are correctly routed.
I just had the same issue. Following the advice from MSDN sorted it for me:
Adorners receive input events just
like any other FrameworkElement.
Because an adorner always has a higher
z-order than the element it adorns,
the adorner receives input events
(such as Drop or MouseMove) that may
be intended for the underlying adorned
element. An adorner can listen for
certain input events and pass these on
to the underlying adorned element by
re-raising the event.
To enable pass-through hit testing of
elements under an adorner, set the hit
test IsHitTestVisible property to
false on the adorner.
i.e In the adorner itself, make sure IsHitTestVisible = false

Resources