I created program to check ability of FrameworkElement to contain TextBox.
namespace MyControls
{
public class FooButton : FrameworkElement
{
public FooButton()
{
Width = 100;
Height = 100;
VisualCollection = new VisualCollection(this);
VisualCollection.Add(new TextBox() { Text = "Meows" });
}
protected override int VisualChildrenCount => VisualCollection.Count;
protected override Visual GetVisualChild(int index) => VisualCollection[index];
public VisualCollection VisualCollection { get; }
}
}
namespace bitmap_test_example
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
Here is XAML file for program:
<Window x:Class="bitmap_test_example.MainWindow"
xmlns:MyControls = "clr-namespace:MyControls"
......
Title="MainWindow" Height="450" Width="800">
<DockPanel>
<MyControls:FooButton></MyControls:FooButton>
<Button>yeah</Button>
</DockPanel>
</Window>
But unfortunately program does not show TextBox. (By the way I checked visual tree, TextBox is indeed contained within custom FooButton)What's the problem with code? Maybe additional methods should be redefined for FrameworkElement?
You should implement the MeasureOverride and ArrangeOverride methods of your custom element. The implementations should measure and arrange the visual children, e.g.:
public class FooButton : FrameworkElement
{
private readonly UIElement _child = new TextBox() { Text = "Meows" };
public FooButton()
{
AddVisualChild(_child);
}
protected override int VisualChildrenCount => 1;
protected override Visual GetVisualChild(int index) => index == 0 ?
_child : throw new ArgumentOutOfRangeException();
protected override Size MeasureOverride(Size availableSize)
{
_child.Measure(availableSize);
return _child.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
_child.Arrange(new Rect(finalSize));
return finalSize;
}
}
Related
I need to draw some charts in WPF with hit testing capability. Following the instructions in the docs I'm drawing the geometries as DrawingVisuals and have a host container implemented for them like this (skipping the hit testing code for brevity):
public class MyVisualHost : FrameworkElement
{
public VisualCollection children;
public MyVisualHost()
{
children = new VisualCollection(this);
}
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];
}
}
and use it in xaml like this
<local:MyVisualHost/>
The user can zoom and scroll the charts, and the DrawingVisuals get updated in a separate thread not to block the UI.
How do I define binding for the children property, so that it was possible to alter it (update DrawingVisuals contained in it) at runtime?
Update
I have just noticed that when you select the xaml element in the xaml editor there is VisualCollection property listed in the Properties panel.
I've tried defining binding for it, but it says:
A 'Binding' cannot be set on the 'VisualCollection' property of type
'...MyVisualHost...'. A 'Binding' can only be set on a
DependencyProperty of a DependencyObject
First of all you will need some public properties on your container...
public class MyVisualHost : FrameworkElement
{
public VisualCollection children;
public MyVisualHost()
{
children = new VisualCollection(this);
children.Add(new Button() {Name = "button"});
children.Add(new TextBox() {Name = "textbox"});
}
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];
}
public int Count => VisualChildrenCount;
public Visual this[int index]
{
get { return GetVisualChild(index); }
}
}
Next define it in XAML like this...
<Window.Resources>
<local:MyVisualHost x:Key="MyVisualHost"/>
</Window.Resources>
Finally bind to the properties like this...
<TextBox Text="{Binding Path=Count, Source={StaticResource MyVisualHost}, Mode=OneWay}"/>
<TextBox Text="{Binding Path=[0].Name, Source={StaticResource MyVisualHost}, Mode=OneWay}"/>
I've set label Content to some custom class:
<Label>
<local:SomeContent x:Name="SomeContent" some="abc" />
</Label>
This properly displays "abc" in a view. However I can't figure out how do I notify the Label that the content property have changed i.e. this:
SomeContent.some = "xyz";
Will not cause the label to update it's view.
I know I can set binding to label's Content property. I have already like 7 different, working methods to achieve automatic update. However I'm interested in this particular behavior because it will save me a ton of work in some scenarios i.e the requirements are:
Label content is always the same SomeContent instance, only it's properties are changed.
No label content binding. The label should take a content object and refresh whenever the content is modified.
Initial value of some property can be set in XAML
some property can be changed in code, causing label refresh.
Am I missing something, or it's not possible?
This is my current implementation of SomeContent:
public class SomeContent : DependencyObject, INotifyPropertyChanged {
public static readonly DependencyProperty someProperty =
DependencyProperty.Register(nameof(some), typeof(string),
typeof(SomeContent),
new PropertyMetadata("", onDPChange)
);
private static void onDPChange(DependencyObject d, DependencyPropertyChangedEventArgs e) {
//throw new NotImplementedException();
(d as SomeContent).some = e.NewValue as String;
}
public event PropertyChangedEventHandler PropertyChanged;
public string some {
get => (string)GetValue(someProperty);
set {
SetValue(someProperty, value);
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(some))
);
}
}
public override string ToString() => some;
}
I turns out it's not possible to do it without third side code. So I wrote a helper class to do it easy now.
Dynamic object
public class SomeContent : IChangeNotifer {
public event Action<object> MODIFIED;
private string _some;
public string some {
get => _some;
set {
_some = value;
MODIFIED?.Invoke(this);
}
}
public override string ToString() => some;
}
You can add it to xaml file and it will be updated automatically. Single additional step is to add UIReseter somewhere bellow the elements that suppose to be auto-updated but that is needed only one for multiple contents in a tree.
Usage
<Window x:Class="DependencyContentTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DependencyContentTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<local:UIReseter />
<Label>
<local:SomeContent x:Name="SomeContent" some="abcd" />
</Label>
<Grid>
<Label>
<local:SomeContent x:Name="nested" some="nyest"/>
</Label>
</Grid>
</StackPanel>
</Window>
MainWindow code
public partial class MainWindow : Window {
private Timer t;
public MainWindow() {
InitializeComponent();
t = new Timer(onTimer, null, 5000, Timeout.Infinite);
MouseDown += (s,e) => { SomeContent.some = "iii"; };
}
private void onTimer(object state) {
Dispatcher.Invoke(() => {
SomeContent.some = "aaaa";
nested.some = "xxx";
});
}
}
And this is the helper class that handles the update
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using H = System.Windows.LogicalTreeHelper;
using FE = System.Windows.FrameworkElement;
using DO = System.Windows.DependencyObject;
using System.Reflection;
using System.Windows.Markup;
namespace DependencyContentTest
{
public interface IChangeNotifer {
/// <summary>Dispatched when this object was modified.</summary>
event Action<object> MODIFIED;
}
/// <summary>This element tracks nested <see cref="IChangeNotifer"/> descendant objects (in logical tree) of this object's parent element and resets a child in it's panel property.
/// Only static (XAML) objects are supported i.e. object added to the tree dynamically at runtime will not be tracked.</summary>
public class UIReseter : UIElement {
public int searchDepth { get; set; } = int.MaxValue;
protected override void OnVisualParentChanged(DO oldParent){
if (VisualParent is FE p) p.Loaded += (s, e) => bind(p);
}
private void bind(FE parent, int dl = 0) {
if (parent == null || dl > searchDepth) return;
var chs = H.GetChildren(parent);
foreach (object ch in chs) {
if (ch is UIReseter r && r != this) throw new Exception($#"There's overlapping ""{nameof(UIReseter)}"" instance in the tree. Use single global instance of check ""{nameof(UIReseter.searchDepth)}"" levels.");
if (ch is IChangeNotifer sc) trackObject(sc, parent);
else bind(ch as FE, ++dl);
}
}
private Dictionary<IChangeNotifer, Reseter> tracked = new Dictionary<IChangeNotifer, Reseter>();
private void trackObject(IChangeNotifer sc, FE parent) {
var cp = getContentProperty(parent);
if (cp == null) return;
var r = tracked.nev(sc, () => new Reseter {
child = sc,
parent = parent,
content = cp,
});
r.track();
}
private PropertyInfo getContentProperty(FE parent) {
var pt = parent.GetType();
var cp = parent.GetType().GetProperties(
BindingFlags.Public |
BindingFlags.Instance
).FirstOrDefault(i => Attribute.IsDefined(i,
typeof(ContentPropertyAttribute)));
return cp ?? pt.GetProperty("Content");
}
private class Reseter {
public DO parent;
public IChangeNotifer child;
public PropertyInfo content;
private bool isTracking = false;
/// <summary>Function called by <see cref="IChangeNotifer"/> on <see cref="IChangeNotifer.MODIFIED"/> event.</summary>
/// <param name="ch"></param>
public void reset(object ch) {
if(! isChildOf(child, parent)) return;
//TODO: Handle multi-child parents
content.SetValue(parent, null);
content.SetValue(parent, child);
}
public void track() {
if (isTracking) return;
child.MODIFIED += reset;
}
private bool isChildOf(IChangeNotifer ch, DO p) {
if(ch is DO dch) {
if (H.GetParent(dch) == p) return true;
child.MODIFIED -= reset; isTracking = false;
return false;
}
var chs = H.GetChildren(p);
foreach (var c in chs) if (c == ch) return true;
child.MODIFIED -= reset; isTracking = false;
return false;
}
}
}
public static class DictionaryExtension {
public static V nev<K,V>(this Dictionary<K,V> d, K k, Func<V> c) {
if (d.ContainsKey(k)) return d[k];
var v = c(); d.Add(k, v); return v;
}
}
}
It could be improved and it not fully tested but it works for current purposes.
Additional problem is that some elements like TextBox cry about not suppopring SomeContent, like it is so hard to use ToString()... but that is another story, and is not related to my question.
Updated answer:
I'd throw away implementing SomeContent as a Dependency property and use a UserControl instead:
<UserControl x:Class="WpfApp1.SomeContent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<TextBlock Text="{Binding some, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SomeContent}}}"/>
</Grid>
</UserControl>
Then in code behind:
/// <summary>
/// Interaction logic for SomeContent.xaml
/// </summary>
public partial class SomeContent : UserControl
{
public static readonly DependencyProperty someProperty =
DependencyProperty.Register(nameof(some), typeof(string),
typeof(SomeContent),
new PropertyMetadata("")
);
public string some
{
get => (string)GetValue(someProperty);
set => SetValue(someProperty, value);
}
public SomeContent()
{
InitializeComponent();
}
}
Next, implement a view model that implements INotifyPropertyChanged:
public class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _somePropertyOnMyViewModel;
public string SomePropertyOnMyViewModel
{
get => _somePropertyOnMyViewModel;
set { _somePropertyOnMyViewModel = value; OnPropertyChanged(); }
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Then create an instance of MyViewModel in your view and assign it to your view's DataContext:
public class MyView : Window
{
public MyView()
{
InitializeComponent();
DataContext = new MyViewModel();
}
}
Then, finally, in MyView use the markup I provided in my original answer:
<Label>
<local:SomeContent x:Name="SomeContent" some="{Binding
SomePropertyOnMyViewModel" />
</Label>
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));
I want to implement my own control derived from FrameworkElement, but the added child elements are not rendered.
I have no idea why.
public class RangeSelection : FrameworkElement
{
private Thumb thumb = null;
#region Construction / Destruction
public RangeSelection()
{
this.thumb = new Thumb();
this.thumb.Width = 32.0;
this.thumb.Height = 32.0;
this.AddVisualChild(this.thumb);
}
#endregion
protected override Size MeasureOverride(Size availableSize)
{
this.thumb.Measure(availableSize);
return new Size(64.0, 64.0);
}
protected override Size ArrangeOverride(Size finalSize)
{
this.thumb.Arrange(new Rect(0, 0, 64.0, 64.0));
return base.ArrangeOverride(finalSize);
}
}
You need to override VisualChildrenCount property and GetVisualChild method. Something like this:
protected override int VisualChildrenCount
{
get { return thumb == null ? 0 : 1; }
}
protected override Visual GetVisualChild(int index)
{
if (_child == null)
{
throw new ArgumentOutOfRangeException();
}
return _child;
}
In case you want more child elements, you should use some kind of collection to store child elements and then you will return collection's count or appropriate element of collection.
I wrote User Control (yay!). But I want it to behave as a container. But wait! I know about
[Designer("System.Windows.Forms.Design.ParentControlDesigner, System.Design",
typeof(IDesigner))]
Trick.
The problem is - I don't want all of my control to behave like container, but only one part. One - de facto - panel ;)
To give wider context: I wrote a control that has Grid, some common buttons, labels and functionalities. But it also has a part where the user is supposed to drop his custom buttons/controls whatever. Only in this particular part of the control, nowhere else.
Anyone had any idea?
You should do the following :
For your user control, you need to create a new designer which enables the inner panel on design-time by calling EnableDesignMode method.
For the inner panel, you need to create a designer which disables moving, resizing and removes some properties from designer.
You should register the designers.
Example
You can read a blog post about this topic here and clone or download a working example:
r-aghaei/ChildContainerControlDesignerSample
Download Zip
Code
Here is the code for different elements of the solution.
Your user control
[Designer(typeof(MyUserControlDesigner))]
public partial class MyUserControl : UserControl
{
public MyUserControl()
{
InitializeComponent();
TypeDescriptor.AddAttributes(this.panel1,
new DesignerAttribute(typeof(MyPanelDesigner)));
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Panel ContentsPanel
{
get { return panel1; }
}
}
Designer for the inner panel
public class MyPanelDesigner : ParentControlDesigner
{
public override SelectionRules SelectionRules
{
get
{
SelectionRules selectionRules = base.SelectionRules;
selectionRules &= ~SelectionRules.AllSizeable;
return selectionRules;
}
}
protected override void PostFilterAttributes(IDictionary attributes)
{
base.PostFilterAttributes(attributes);
attributes[typeof(DockingAttribute)] =
new DockingAttribute(DockingBehavior.Never);
}
protected override void PostFilterProperties(IDictionary properties)
{
base.PostFilterProperties(properties);
var propertiesToRemove = new string[] {
"Dock", "Anchor", "Size", "Location", "Width", "Height",
"MinimumSize", "MaximumSize", "AutoSize", "AutoSizeMode",
"Visible", "Enabled",
};
foreach (var item in propertiesToRemove)
{
if (properties.Contains(item))
properties[item] = TypeDescriptor.CreateProperty(this.Component.GetType(),
(PropertyDescriptor)properties[item],
new BrowsableAttribute(false));
}
}
}
Designer for your user control
public class MyUserControlDesigner : ParentControlDesigner
{
public override void Initialize(IComponent component)
{
base.Initialize(component);
var contentsPanel = ((MyUserControl)this.Control).ContentsPanel;
this.EnableDesignMode(contentsPanel, "ContentsPanel");
}
public override bool CanParent(Control control)
{
return false;
}
protected override void OnDragOver(DragEventArgs de)
{
de.Effect = DragDropEffects.None;
}
protected override IComponent[] CreateToolCore(ToolboxItem tool, int x,
int y, int width, int height, bool hasLocation, bool hasSize)
{
return null;
}
}