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!!!
Related
I want do draw a custom string on the bottom right of an button with an PlatformEffect. Prefer to use an effect to be more flexible and apply this only to specific buttons and not application wide. The buttons are created dynamically without xaml.
Is this possible or do i need to create a custom button + renderer?
Thanks.
You could create your Custom button and then set the text alignment via custom renderer.
[assembly: ExportRenderer(typeof(Xamarin.Forms.Button), typeof(ButtonCustomRenderer))]//set the Button as your custom button
namespace App3.Droid
{
public class ButtonCustomRenderer : ButtonRenderer
{
public ButtonCustomRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e)
{
base.OnElementChanged(e);
Control.Gravity = GravityFlags.End // Set the horizontal text alignment to right
| GravityFlags.Bottom; // Set the vertical text alignment to bottom
}
}
}
I solved it by creating a renderer which overrides the Draw method.
public class HotkeyButtonRenderer : ButtonRenderer
{
HotkeyButton element;
public HotkeyButtonRenderer(Context ctx) : base(ctx)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
element = Element as HotkeyButton;
SetWillNotDraw(false);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (element != null && e.PropertyName == HotkeyButton.HotkeyTextProperty.PropertyName)
{
Invalidate();
}
}
public override void Draw(Canvas canvas)
{
base.Draw(canvas);
if (element != null && !string.IsNullOrEmpty(element.HotkeyText))
{
string textToDraw = string.Format("({0})", element.HotkeyText);
float textSize = Control.TextSize / 1.5f;
SizeF measuredTextSize = GetTextSize(textToDraw, element.FontFamily, textSize);
float x = 5;
float y = Height-measuredTextSize.Height/2;
canvas.DrawText(textToDraw,
x,
y,
new TextPaint
{
Color = element.BorderColor.ToAndroid(),
TextSize = textSize
});
}
}
private SizeF GetTextSize(string text, string fontFamily, float textSize)
{
var textPaint = new SKPaint(new SKFont(SKTypeface.FromFamilyName(fontFamily), textSize));
SKRect textBounds = new SKRect();
textPaint.MeasureText(text, ref textBounds);
return new SizeF(textBounds.Width, textBounds.Height);
}
}
public class HotkeyButton : Button
{
public static readonly BindableProperty HotkeyTextProperty = BindableProperty.Create(
propertyName: nameof(HotkeyText),
returnType: typeof(string),
declaringType: typeof(MobileEntry),
defaultValue: string.Empty);
public string HotkeyText
{
get { return (string)GetValue(HotkeyTextProperty); }
set { SetValue(HotkeyTextProperty, value); }
}
}
I have been trying to set a bindable property value in my Element from my native control through a custom renderer. My native control is a view (painview) where you can draw and I am trying to get the drawing and set it, as a base64 string, to a bindable property Signature in my Element.
This is my Native Control
public class PaintView : View
{
Canvas _drawCanvas;
Bitmap _canvasBitmap;
readonly Paint _paint;
readonly Dictionary<int, MotionEvent.PointerCoords> _coords = new Dictionary<int, MotionEvent.PointerCoords>();
public Bitmap CanvasBitmap { get => _canvasBitmap; private set => _canvasBitmap = value; }
private readonly string TAG = nameof(PaintView);
public event EventHandler OnLineDrawn;
public PaintView(Context context) : base(context, null, 0)
{
_paint = new Paint() { Color = Color.Blue, StrokeWidth = 5f, AntiAlias = true };
_paint.SetStyle(Paint.Style.Stroke);
}
public PaintView(Context context, IAttributeSet attrs) : base(context, attrs) { }
public PaintView(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle) { }
protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
{
base.OnSizeChanged(w, h, oldw, oldh);
_canvasBitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888); // full-screen bitmap
_drawCanvas = new Canvas(_canvasBitmap); // the canvas will draw into the bitmap
}
public override bool OnTouchEvent(MotionEvent e)
{
switch (e.ActionMasked)
{
case MotionEventActions.Down:
{
int id = e.GetPointerId(0);
var start = new MotionEvent.PointerCoords();
e.GetPointerCoords(id, start);
_coords.Add(id, start);
return true;
}
case MotionEventActions.PointerDown:
{
int id = e.GetPointerId(e.ActionIndex);
var start = new MotionEvent.PointerCoords();
e.GetPointerCoords(id, start);
_coords.Add(id, start);
return true;
}
case MotionEventActions.Move:
{
for (int index = 0; index < e.PointerCount; index++)
{
var id = e.GetPointerId(index);
float x = e.GetX(index);
float y = e.GetY(index);
_drawCanvas.DrawLine(_coords[id].X, _coords[id].Y, x, y, _paint);
_coords[id].X = x;
_coords[id].Y = y;
OnLineDrawn?.Invoke(this, EventArgs.Empty);
}
Invalidate();
return true;
}
case MotionEventActions.PointerUp:
{
int id = e.GetPointerId(e.ActionIndex);
_coords.Remove(id);
return true;
}
case MotionEventActions.Up:
{
int id = e.GetPointerId(0);
_coords.Remove(id);
return true;
}
default:
return false;
}
}
protected override void OnDraw(Canvas canvas)
{
// Copy the off-screen canvas data onto the View from it's associated Bitmap (which stores the actual drawn data)
canvas.DrawBitmap(_canvasBitmap, 0, 0, null);
}
public void Clear()
{
_drawCanvas.DrawColor(Color.Black, PorterDuff.Mode.Clear); // Paint the off-screen buffer black
Invalidate(); // Call Invalidate to redraw the view
}
public void SetInkColor(Color color)
{
_paint.Color = color;
}
}
The property PaintView._canvasBitmap is the one I want to be set in my Xamarin.Form Element through my custom renderer.
This is my Custom Renderer
public class SketchViewRenderer : ViewRenderer<SketchView, PaintView>
{
public SketchViewRenderer(Context context) : base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<SketchView> e)
{
if (Control == null)
{
var paintView = new PaintView(Context);
paintView.SetInkColor(Element.InkColor.ToAndroid());
SetNativeControl(new PaintView(Context));
MessagingCenter.Subscribe<SketchView>(this, nameof(SketchView.OnClear), OnMessageClear);
Control.OnLineDrawn += PaintViewLineDrawn;
}
}
private void PaintViewLineDrawn(object sender, EventArgs e)
{
var sketchCrl = (ISketchViewController)Element;
if (sketchCrl == null) return;
try
{
Element.SetValueFromRenderer(SketchView.SignatureProperty, Utils.Utils.BitmapToBase64(Control.CanvasBitmap));
sketchCrl.SendSketchUpdated(Utils.Utils.BitmapToBase64(Control.CanvasBitmap));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == SketchView.InkColorProperty.PropertyName)
{
Control.SetInkColor(Element.InkColor.ToAndroid());
}
if (e.PropertyName == SketchView.ClearProperty.PropertyName)
{
if (Element.Clear) OnMessageClear(Element);
}
}
private void OnMessageClear(SketchView sender)
{
if (sender == Element) Control.Clear();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
MessagingCenter.Unsubscribe<SketchView>(this, nameof(SketchView.OnClear));
Control.OnLineDrawn -= PaintViewLineDrawn;
}
base.Dispose(disposing);
}
}
I have tried changing my Element.Signature property through the SketchViewRenderer.PaintViewLineDrawn(...) method without success. This has been prove when debugging my view model where the property has not been set as expected.
My Xamarin.Forms Element looks as follow
public class SketchView : View, IDoubleTappedController, ISketchViewController
{
public static readonly BindableProperty SignatureProperty = BindableProperty.Create(nameof(Signature), typeof(string), typeof(SketchView), null, defaultBindingMode: BindingMode.TwoWay);
public string Signature
{
get => (string)GetValue(SignatureProperty);
set => SetValue(SignatureProperty, value);
}
public static readonly BindableProperty MultiTouchEnabledProperty = BindableProperty.Create(nameof(MultiTouchEnabled), typeof(bool), typeof(SketchView), false);
public bool MultiTouchEnabled
{
get => (bool)GetValue(MultiTouchEnabledProperty);
set => SetValue(MultiTouchEnabledProperty, value);
}
public static readonly BindableProperty InkColorProperty = BindableProperty.Create(nameof(InkColor), typeof(Xamarin.Forms.Color), typeof(SketchView), Xamarin.Forms.Color.Azure);
public Xamarin.Forms.Color InkColor
{
get => (Xamarin.Forms.Color)GetValue(InkColorProperty);
set => SetValue(InkColorProperty, value);
}
public static readonly BindableProperty ClearProperty = BindableProperty.Create(nameof(Clear), typeof(bool), typeof(SketchView), false, defaultBindingMode: BindingMode.TwoWay);
public bool Clear
{
get => (bool)GetValue(ClearProperty);
set
{
SetValue(ClearProperty, value);
if (value) { OnClear(); }
}
}
public void OnClear()
{
MessagingCenter.Send(this, nameof(OnClear));
}
public void SetSignature(string signature)
{
Signature = signature;
}
void IDoubleTappedController.DoubleTapped()
{
throw new NotImplementedException();
}
void ISketchViewController.SendSketchUpdated(string signature)
{
Clear = false;
Signature = signature;
}
}
I have also tried using the SetValueFromRenderer() method from my Custom renderer, again, without success.
May you suggest to me what is the way to set an Element value from a Custom Renderer?
Thanks and kind regards,
Temo
The problem was that the field in my view model was set to null when comparing it with the value. Then throwing a TargetException letting the source buggy unable to be updated by the target.
public bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = default)
{
if (value == null) return false;
if (field != null && field.Equals(value)) return false;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
Now, I make sure the field is not null before using the Equals operator.
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));
Trying to make an equation editor like that in Microsoft Word in C# and WPF. XML cannot be used; it has to be purely programmatic.
Right now I have LineGUIObject : System.Windows.Controls.WrapPanel, which is like System.Windows.Controls.TextBox, except that instead of just showing strings it shows each element of a List<System.Windows.UIElement> in order.
Now I want for a user to be able to click on an instance of LineGUIObject and type into it. The holdup is that I don't know how to capture the user's click or read the input that they type. How can this be done?
Note: This question is not asking how to handle input once captured; just how to get the input in the first place. For example, is there some event that fires off after the user clicks it or something? I can't seem to find one for System.Windows.Controls.WrapPanel, which might imply that I need to use another type of object, or..?
Current code:
public class LineGUIObject
: System.Windows.Controls.WrapPanel
{
private List<System.Windows.UIElement> _uiElementList;
private CursorGUIObject _cursor;
private int? _cursorIndex;
public LineGUIObject(System.Windows.Threading.Dispatcher dispatcher)
: base()
{
this.UIElementList = new List<System.Windows.UIElement>();
this.Cursor = new CursorGUIObject(dispatcher, 25, 1.5, 250);
this.UIElementList.Add(this.Cursor);
this.AddText("[junk string just to see this otherwise invisible object while debugging]");
}
protected void InterpretUserKeyStroke(/* ??? */)
{
//How do we get this method to be called on user input,
//e.g. when the user types "1"?
throw new NotImplementedException();
}
protected void AddText(string text)
{
this.UIElementList.Add(new System.Windows.Controls.TextBlock(new System.Windows.Documents.Run(text)));
this.UpdateDisplay();
}
protected List<System.Windows.UIElement> UIElementList { get { return this._uiElementList; } private set { this._uiElementList = value; } }
protected CursorGUIObject Cursor { get { return this._cursor; } private set { this._cursor = value; } }
protected int? CursorIndex
{
get { return this._cursorIndex; }
set
{
int? nullablePriorIndex = this.CursorIndex;
if (nullablePriorIndex != null)
{
int priorIndex = nullablePriorIndex.Value;
this.UIElementList.RemoveAt(priorIndex);
}
if (value == null)
{
this._cursorIndex = null;
}
else
{
int newIndex = value.Value;
if (newIndex < 0)
{
newIndex = 0;
}
else
{
int thisListCount = this.UIElementList.Count;
if (newIndex > thisListCount) { newIndex = thisListCount; }
}
this.UIElementList.Insert(newIndex, this.Cursor);
this._cursorIndex = newIndex;
}
this.UpdateDisplay();
}
}
protected void UpdateDisplay()
{
this.Children.Clear();
foreach (System.Windows.UIElement uiElement in this.UIElementList) { this.Children.Add(uiElement); }
}
}
public class CursorGUIObject
: System.Windows.Controls.WrapPanel
{
public const double MINIMUM_BLINK_TIME_IN_MS = 5;
public const double MINIMUM_HEIGHT = 0.5;
public const double MINIMUM_WIDTH = 0.5;
private object ToggleVisibilityLock = new object();
private delegate void TimerIntervalDelegate();
private System.Windows.Shapes.Rectangle _rectangle;
private System.Timers.Timer _timer;
private System.Windows.Threading.Dispatcher _dispatcher;
public CursorGUIObject(System.Windows.Threading.Dispatcher dispatcher, double height, double width, double blinkTimeInMS)
{
this.Dispatcher = dispatcher;
System.Windows.Shapes.Rectangle rectangle = new System.Windows.Shapes.Rectangle();
rectangle.Width = width > MINIMUM_WIDTH ? width : MINIMUM_WIDTH;
rectangle.Height = height > MINIMUM_HEIGHT ? height : MINIMUM_HEIGHT;
rectangle.Fill = System.Windows.Media.Brushes.Black;
this.Rectangle = rectangle;
this.Children.Add(rectangle);
System.Timers.Timer timer = new System.Timers.Timer(blinkTimeInMS > MINIMUM_BLINK_TIME_IN_MS ? blinkTimeInMS : MINIMUM_BLINK_TIME_IN_MS);
this.Timer = timer;
timer.Elapsed += timer_Elapsed;
timer.Start();
}
~CursorGUIObject()
{
System.Timers.Timer timer = this.Timer;
if (timer != null) { timer.Dispose(); }
}
private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
Delegate timerDelegate = new TimerIntervalDelegate(ToggleVisibility);
this.Dispatcher.BeginInvoke(timerDelegate);
}
protected void ToggleVisibility()
{
lock (ToggleVisibilityLock)
{
if (this.Rectangle.Visibility.Equals(System.Windows.Visibility.Hidden))
{
this.Rectangle.Visibility = System.Windows.Visibility.Visible;
}
else
{
this.Rectangle.Visibility = System.Windows.Visibility.Hidden;
}
}
}
protected System.Windows.Shapes.Rectangle Rectangle { get { return this._rectangle; } private set { this._rectangle = value; } }
protected System.Timers.Timer Timer { get { return this._timer; } private set { this._timer = value; } }
protected System.Windows.Threading.Dispatcher Dispatcher { get { return this._dispatcher; } private set { this._dispatcher = value; } }
}
Pretty much all WPF controls provide access to the UIElement.PreviewMouseDown Event, which you can use to monitor mouse clicks. So, this event lets you monitor when each object is clicked on. Next, I'd advise you to use a small Popup control to popup a TextBox that the user could enter a value with:
<Popup Name="Popup">
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="5" Padding="5">
<TextBox Text="{Binding InputText}" />
</Border>
</Popup>
Depending on how you have set up your project, you could open the Popup from the event handler:
private void YourObject_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Popup.IsOpen = true;
}
Turns out that LineGUIObject just needed to have this.Focusable = true; set in its constructor so that it could receive the keyboard's focus when clicked.
Now that it can be focused on, this.KeyUp += LineGUIObject_KeyUp; also in the constructor, and
protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
{
this.AddText(e.Key.ToString());
}
Even this had a problem at first since my LineGUIObject was nested in a ScrollViewer which kept stealing focus immediately after the LineGUIObject would receive it. This was fixed by making the ScrollViewer to be unable to get focus, i.e. <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Focusable="False"/>.
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;
}
}