How does a container know when a child has called InvalidateArrange? - c#

I'm trying to learn the basics of creating a custom panel in a WinRT XAML app. I have defined an attached dependency property and it's working as expected except i can't figure out how to get the property's callback for a child element to trigger the arrange or measure of the container.
What's the proper way to for a child to let it's container know that arrange and measure should be called again? In my WPF 4 unleashed book they use the FrameworkPropertyMetadataOptions.AffectsParentArrange but that doesn't seem to be available in WinRT.
public class SimpleCanvas : Panel
{
#region Variables
#region Left Property
public static double GetLeft(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
object value = element.GetValue(LeftProperty);
Type valueType = value.GetType();
return Convert.ToDouble(value);
}
public static void SetLeft(UIElement element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(LeftProperty, value);
}
public static readonly DependencyProperty LeftProperty =
DependencyProperty.RegisterAttached("Left", typeof(double), typeof(SimpleCanvas),
new PropertyMetadata(0, OnLeftPropertyChanged));
public static void OnLeftPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)source;
// This doesn't cause ArrangeOverride below to be called
element.InvalidateArrange();
}
#endregion
#region Top Property
public static double GetTop(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
object value = element.GetValue(TopProperty);
return (value == null) ? 0 : (double)value;
}
public static void SetTop(UIElement element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(TopProperty, value);
}
public static readonly DependencyProperty TopProperty =
DependencyProperty.RegisterAttached("Top", typeof(double), typeof(SimpleCanvas),
new PropertyMetadata(0, OnTopPropertyChanged));
public static void OnTopPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)source;
// This doesn't cause ArrangeOverride below to be called
element.InvalidateArrange();
}
#endregion
#endregion
public SimpleCanvas()
{
}
#region Methods
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement child in this.Children)
{
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
}
return new Size(0, 0);
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement child in this.Children)
{
double x = 0;
double y = 0;
double left = GetLeft(child);
double top = GetTop(child);
if (!double.IsNaN(left))
{
x = left;
}
if (!double.IsNaN(top))
{
y = top;
}
child.Arrange(new Rect(new Point(x, y), child.DesiredSize));
}
return finalSize;
}
#endregion
}

I'm late to the party, but I went the same direction and faced the same issue. Here is my solution.
In your callback you call InvalidateArrange on the child element you attached you property to:
public static void OnTopPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)source;
// This doesn't cause ArrangeOverride below to be called
element.InvalidateArrange();
}
But you should really invalidate the panel, by changing you code so:
public static void OnTopPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
UIElement panel= VisualTreeHelper.GetParent(source) as UIElement;
if(panel != null)
panel.InvalidateArrange();
}
And it should work (did for me).

If InvalidateArrange alone doesn't work you could also try InvalidateMeasure or UpdateLayout.

I had this problem with a child control which depended on the FontSize of the parent control. I solved the problem by traveling up the stack of parents and invalidating everything:
static MyControl()
{
// replace base implementation of the dependent property
FontSizeProperty.OverrideMetadata(typeof(Scalar),
new FrameworkPropertyMetadata(SystemFonts.MessageFontSize, FrameworkPropertyMetadataOptions.Inherits, OnMeasureInvalidated));
}
private static void OnMeasureInvalidated(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
// recurse over parent stack
while (true)
{
var parent = VisualTreeHelper.GetParent(sender) as UIElement;
if (parent == null) return; // break on root element
parent.InvalidateMeasure();
sender = parent;
}
}

Related

Drop event fires twice on inherited panel

I´ve created my own control inherited from the panel control with OnDrop method for handling Drop event. Unfortunately, the event is fired twice.
I don´t know why.
here my code
public partial class Panel_my : Panel
{
public static readonly DependencyProperty StartDateProperty =
DependencyProperty.RegisterAttached("StartDate", typeof(DateTime), typeof(Panel_my), new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.AffectsParentArrange));
public static readonly DependencyProperty EndDateProperty =
DependencyProperty.RegisterAttached("EndDate", typeof(DateTime), typeof(Panel_my), new FrameworkPropertyMetadata(DateTime.MaxValue, FrameworkPropertyMetadataOptions.AffectsParentArrange));
public static readonly DependencyProperty MaxDateProperty =
DependencyProperty.Register("MaxDate", typeof(DateTime), typeof(Panel_my), new FrameworkPropertyMetadata(DateTime.MaxValue, FrameworkPropertyMetadataOptions.AffectsMeasure));
public static readonly DependencyProperty MinDateProperty =
DependencyProperty.Register("MinDate", typeof(DateTime), typeof(Panel_my), new FrameworkPropertyMetadata(DateTime.MaxValue, FrameworkPropertyMetadataOptions.AffectsMeasure));
public static DateTime GetStartDate(DependencyObject obj)
{
return (DateTime)obj.GetValue(StartDateProperty);
}
public static void SetStartDate(DependencyObject obj, DateTime value)
{
obj.SetValue(StartDateProperty, value);
}
public static DateTime GetEndDate(DependencyObject obj)
{
return (DateTime)obj.GetValue(EndDateProperty);
}
public static void SetEndDate(DependencyObject obj, DateTime value)
{
obj.SetValue(EndDateProperty, value);
}
public DateTime MaxDate
{
get { return (DateTime)GetValue(MaxDateProperty); }
set { SetValue(MaxDateProperty, value); }
}
public DateTime MinDate
{
get { return (DateTime)GetValue(MinDateProperty); }
set { SetValue(MinDateProperty, value); }
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement child in Children)
{
child.Measure(availableSize);
}
return new Size(0, 0);
}
protected override Size ArrangeOverride(Size finalSize)
{
double range = (MaxDate - MinDate).Ticks;
double pixelsPerTick = finalSize.Width / range;
foreach (UIElement child in Children)
{
ArrangeChild(child, MinDate, pixelsPerTick, finalSize.Height);
}
return finalSize;
}
private void ArrangeChild(UIElement child, DateTime minDate, double pixelsPerTick, double elementHeight)
{
DateTime childStartDate = GetStartDate(child);
DateTime childEndDate = GetEndDate(child);
TimeSpan childDuration = childEndDate - childStartDate;
double offset = (childStartDate - minDate).Ticks * pixelsPerTick;
double width = childDuration.Ticks * pixelsPerTick;
child.Arrange(new Rect(offset, 0, width, elementHeight));
}
protected override void OnDrop(DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.StringFormat))
{
}
e.Handled = true;
}
}
When I place a button on the panel and move it in the drag & drop process and drop it on that panel, the drop event is fired twice.
What can I do to have only one drop event?
I got it. The problem is that the button fires event MouseMove not only when the mouse is moving but even when the left button is pressed without moving the mouse.
I changed the method OnMouseMove and than it works!
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed && this.IsPressed == false)
{
DragDrop.DoDragDrop(this, this.Content, DragDropEffects.Move);
}
}
Thank you, #jdweng very much for help and direction!!!

Change attached property from CustomPanel doesn't refresh panel

I created dummy custom panel ShelfPanel with attached property Exact influence panel arrange:
class ShelfPanel : Panel
{
#region Start attached property
public static DependencyProperty ExactProperty = DependencyProperty.RegisterAttached("Exact", typeof(int), typeof(ShelfPanel),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.AffectsArrange | // Does this options have auto action?
FrameworkPropertyMetadataOptions.AffectsRender)); // Does this options have auto action?
public static void SetExact(UIElement element, int value)
{
element.SetValue(ExactProperty, value);
}
public static int GetExact(UIElement element)
{
return (int)element.GetValue(ExactProperty);
}
#endregion
protected override Size MeasureOverride(Size availableSize)
{
Size final = new Size();
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
final.Height += child.DesiredSize.Height;
}
return final;
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement child in InternalChildren)
{
Point position = new Point();
int exact = ShelfPanel.GetExact(child);
// Calculate position based on attached Exact
position.X = exact * 100;
position.Y = exact * 100;
child.Arrange(new Rect(position, child.DesiredSize));
}
return finalSize;
}
}
<local:ShelfPanel>
<local:Box local:ShelfPanel.Exact="0" MouseDown="Box_MouseDown"/>
<local:Box local:ShelfPanel.Exact="1" />
<local:Box local:ShelfPanel.Exact="2" />
</local:ShelfPanel>
public partial class MainWindow : Window // Codebehind for previous xaml
{
public MainWindow()
{
InitializeComponent();
}
private void Box_MouseDown(object sender, MouseButtonEventArgs e)
{
// I certainly sure this code get triggered after click.
ShelfPanel.SetExact(sender as UIElement, 3);
}
}
This works perfect <local:Box> are arranged as planned.
As you can deduce from code, after click on first <local:Box> it should change it position to 3, just after others 2. But surprisingly nothing happen.
Doesn't FrameworkPropertyMetadataOptions.Affects Arrange or FrameworkPropertyMetadataOptions.AffectsRender automatically repaint panel?
To make this works I need to add PropertyChangedCallbackand call there InvalidateVisual()?
You are setting the attached property on a Box, but want the parent element of the Box, i.e. the ShelfPanel to be arranged.
You should therefore set FrameworkPropertyMetadataOptions.AffectsParentArrange:
public static readonly DependencyProperty ExactProperty =
DependencyProperty.RegisterAttached("Exact", typeof(int), typeof(ShelfPanel),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.AffectsParentArrange));

Access subclass of FrameworkElement from VisualTreeHelper.HitTest

I have a custom element class that is a subclass of FrameworkElement.
public class MyCustomElement : FrameworkElement
{
private VisualCollection children;
public MyCustomElement()
{
this.children = new VisualCollection(this);
this.children.Add(MyDrawingRoutines());
}
private DrawingVisual MyDrawingRoutines()
{
//...
}
protected override int VisualChildrenCount
{
get { return children.Count; }
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= children.Count)
{
throw new ArgumentOutOfRangeException();
}
return children[index];
}
}
The UI holds a canvas in which these custom drawing elements are added and hit testing is performed.
public partial class MainWindow : Window
{
private MyCustomElement myCustomElement;
public MainWindow()
{
InitializeComponent();
myCustomElement = new MyCustomElement();
myCanvas.Children.Add(myCustomElement);
}
private void myCanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
System.Windows.Point pt = e.GetPosition((UIElement)sender);
VisualTreeHelper.HitTest(this, null, new HitTestResultCallback(leftClickCallback), new PointHitTestParameters(pt));
}
public HitTestResultBehavior leftClickCallback(HitTestResult result)
{
if (result.VisualHit.GetType() == typeof(DrawingVisual))
{
if (((DrawingVisual)result.VisualHit).Opacity == 1.0)
{
((DrawingVisual)result.VisualHit).Opacity = 0.4;
}
else
{
((DrawingVisual)result.VisualHit).Opacity = 1.0;
}
}
return HitTestResultBehavior.Stop;
}
}
}
This code works as expected, but I cannot find a way to determine which MyCustomElement the detected DrawingVisual belongs. Right now, the opacity adjustment is done only superficially correct? I would like to change the opacity property on MyCustomElement, have the MyDrawingRoutines() method apply it, and have only the finished DrawingVisual drawn on the Canvas.
You should be able to cast the Parent property of the DrawingVisual to your MyCustomElement class:
public HitTestResultBehavior leftClickCallback(HitTestResult result)
{
var visual = result.VisualHit as DrawingVisual;
if (visual != null)
{
var element = visual.Parent as MyCustomElement;
if (element != null)
{
if (element.Opacity == 1.0)
{
element.Opacity = 0.4;
}
else
{
element.Opacity = 1.0;
}
}
}
return HitTestResultBehavior.Stop;
}
In case you need to get the parent of any visual (not just a ContainerVisual, which has the Parent property as shown above), you may use VisualTreeHelper.GetParent:
var visual = result.VisualHit;
var element = VisualTreeHelper.GetParent(visual) as MyCustomElement;

Why VisualTreeHelper.GetChildrenCount() returns 0 for Popup?

I move focus to the Popup on its opening:
wcl:FocusHelper.IsFocused="{Binding RelativeSource={RelativeSource Self}, Path=IsOpen}"
FocusHelper class code:
public static class FocusHelper
{
public static readonly DependencyProperty IsFocusedProperty =
DependencyProperty.RegisterAttached("IsFocused", typeof(bool?), typeof(FocusHelper), new FrameworkPropertyMetadata(IsFocusedChanged));
public static bool? GetIsFocused(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (bool?)element.GetValue(IsFocusedProperty);
}
public static void SetIsFocused(DependencyObject element, bool? value)
{
if (element == null)
throw new ArgumentNullException("element");
element.SetValue(IsFocusedProperty, value);
}
private static void IsFocusedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var fe = (FrameworkElement)d;
if (e.OldValue == null)
{
fe.GotFocus += ElementGotFocus;
fe.LostFocus += ElementLostFocus;
fe.IsVisibleChanged += ElementIsVisibleChanged;
}
if (e.NewValue == null)
{
fe.GotFocus -= ElementGotFocus;
fe.LostFocus -= ElementLostFocus;
fe.IsVisibleChanged -= ElementIsVisibleChanged;
return;
}
if ((bool)e.NewValue)
{
fe.SetFocusWithin();
}
}
private static void ElementIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var fe = (FrameworkElement)sender;
if (fe.IsVisible
&& (bool)(((FrameworkElement) sender).GetValue(IsFocusedProperty))) // Bring focus to just become visible element.
fe.Focus();
}
private static void ElementGotFocus(object sender, RoutedEventArgs e)
{
((FrameworkElement)sender).SetCurrentValue(IsFocusedProperty, true);
}
private static void ElementLostFocus(object sender, RoutedEventArgs e)
{
((FrameworkElement)sender).SetCurrentValue(IsFocusedProperty, false);
}
/// <summary>
/// Tries to set focus to the element or any other element inside this one.
/// Tab index is respected
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static bool SetFocusWithin(this DependencyObject element)
{
if (element == null)
throw new ArgumentNullException("element");
var inputElement = element as IInputElement;
if (inputElement == null || !inputElement.Focus())
{
var children = element.GetVisualChildrenSortedByTabIndex().Where(child => !(child is Control) || (bool)child.GetValue(Control.IsTabStopProperty) );
return children.Any(SetFocusWithin);
}
return true;
}
}
ElementTreeHelper class part :
public static IEnumerable<DependencyObject> GetVisualChildren(this DependencyObject parent)
{
if (parent == null)
throw new ArgumentNullException("parent");
var count = VisualTreeHelper.GetChildrenCount(parent);
for (var i = 0; i < count; i++)
yield return VisualTreeHelper.GetChild(parent, i);
}
public static IEnumerable<DependencyObject> GetVisualChildrenSortedByTabIndex(this DependencyObject parent)
{
if (parent == null)
throw new ArgumentNullException("parent");
return parent.GetVisualChildren().OrderBy(KeyboardNavigation.GetTabIndex);
}
The problem is that
var count = VisualTreeHelper.GetChildrenCount(parent) == 0 when parent is Popup.
UPDATE
The answer is here
The Popup doesn't host the Child within itself. Instead it creates a PopupRoot (internal class) that is the root visual of a new HWND created to host the popup's content within a separate top level window (or child window in xbap). The Child of the Popup is hosted within an AdornerDecorator within that PopupRoot.
I'm a bit late to the party, but AndrewS's answer helped me to give up on using GetChildrenCount on my popup. I did notice that the Child property of the Popup is correctly populated so i made a separate code path for finding a specific child type of a Popup object. Here's the code i use to find a specific child by type (and optionally by name):
public static TChildItem FindVisualChild<TChildItem>(this DependencyObject dependencyObject, String name) where TChildItem : DependencyObject
{
// Search immediate children first (breadth-first)
var childrenCount = VisualTreeHelper.GetChildrenCount(dependencyObject);
//http://stackoverflow.com/questions/12304904/why-visualtreehelper-getchildrencount-returns-0-for-popup
if (childrenCount == 0 && dependencyObject is Popup)
{
var popup = dependencyObject as Popup;
return popup.Child != null ? popup.Child.FindVisualChild<TChildItem>(name) : null;
}
for (var i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(dependencyObject, i);
var nameOfChild = child.GetValue(FrameworkElement.NameProperty) as String;
if (child is TChildItem && (name == String.Empty || name == nameOfChild))
return (TChildItem)child;
var childOfChild = child.FindVisualChild<TChildItem>(name);
if (childOfChild != null)
return childOfChild;
}
return null;
}
public static TChildItem FindVisualChild<TChildItem>(this DependencyObject dependencyObject) where TChildItem : DependencyObject
{
return dependencyObject.FindVisualChild<TChildItem>(String.Empty);
}

How to detect scrolling at the end of a listbox for N lists?

I found this approach by slimcode which creates this:
public static readonly DependencyProperty ListVerticalOffsetProperty = DependencyProperty.Register(
"ListVerticalOffset",
typeof( double ),
typeof( SearchBusinessResultsPage ),
new PropertyMetadata( new PropertyChangedCallback( OnListVerticalOffsetChanged ) ) );
public double ListVerticalOffset
{
get { return ( double )this.GetValue( ListVerticalOffsetProperty ); }
set { this.SetValue( ListVerticalOffsetProperty, value ); }
}
private static void OnListVerticalOffsetChanged( DependencyObject obj, DependencyPropertyChangedEventArgs e )
{
// ...
}
It creates a single readonly property and a single static method to handle it. But I want to make N lists handle an event at the end of the scroll.
I don't know how to use this code to handle different lists...
Is there a better way of doing it? And how can I use the same thing for different lists?
You can create a BusinessListBox class that inherits from a ListBox and define its style and template as in the sample mentioned. You can then extend the DependencyProperty change handlers to have an instance change handler method.
private static void OnListVerticalOffsetChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var target = (BusinessListBox)d;
double oldListVerticalOffset = (double)e.OldValue;
double newListVerticalOffset = target.ListVerticalOffset;
target.OnListVerticalOffsetChanged(oldListVerticalOffset, newListVerticalOffset);
}
private void OnListVerticalOffsetChanged(
double oldListVerticalOffset, double newListVerticalOffset)
{
}
Ideally you would just implement a Behavior or attached property that would allow you to do the same on a regular ListBox.
All you need to do is add a handler for ScrollChangedEvent detection and use VerticalOffset property to find whether the scrolling has reached the end of the scroll.
listBox.AddHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler((o , e) => {
ListBox lb = (ListBox) o;
ScrollViewer sv = GetDescendantByType(lb, typeof(ScrollViewer)) as ScrollViewer;
if (sv.ScrollableHeight == sv.VerticalOffset) {
//End of the scroll reached
}
}));
The helper method for finding the ScrollViewer in the ListBox
/***
* Helper method to traverse the root to find the the element of type `type`
***/
public static Visual GetDescendantByType(Visual element, Type type) {
if (element == null) {
return null;
}
if (element.GetType() == type) {
return element;
}
Visual foundElement = null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++) {
Visual visual = VisualTreeHelper.GetChild(element, i) as Visual;
foundElement = GetDescendantByType(visual, type);
if (foundElement != null) {
break;
}
}
return foundElement;
}

Categories