How to create binding for FrameworkElement VisualCollection - c#

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}"/>

Related

Why does custom FrameworkElement not show children?

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;
}
}

Validation.ErrorEvent is not rised when element with error is removed

I have to show a list where each item will be validated. I am subscribed to Validation.ErrorEvent on a top level to monitor for children.
When I remove item with validation error from list this event is not rised.
In example below I have 3 TextBox on screen, each is bound to int property. Entering wrong value will fire event (Title is changed to "+"), fixing value afterwards will fire event once (Title is changed to "-").
However removing TextBox while having error will not rise event (to clean up) and Title stay "+":
How can I fix that? Ideally I want that this event is automatically rised before removing happens.
Please note: in real project there is complex hierarchy of view models, solutions like "set Title in delete method" would require monitoring for sub-views and propagating that info through all hierarchy, which I'd like to avoid. I'd prefer view-only solution.
MCVE:
public partial class MainWindow : Window
{
public ObservableCollection<VM> Items { get; } = new ObservableCollection<VM> { new VM(), new VM(), new VM() };
public MainWindow()
{
InitializeComponent();
AddHandler(Validation.ErrorEvent, new RoutedEventHandler((s, e) =>
Title = ((ValidationErrorEventArgs)e).Action == ValidationErrorEventAction.Added ? "+" : "-"));
DataContext = this;
}
void Button_Click(object sender, RoutedEventArgs e) => Items.RemoveAt(0);
}
public class VM
{
public int Test { get; set; }
}
xaml:
<StackPanel>
<ItemsControl ItemsSource="{Binding Items}" Height="200">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Test, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Remove first" Click="Button_Click" />
</StackPanel>
After a little time, I have a working solution that you could start with. As already mentioned by Ed, You're using the UI as a data structure, which is never a good idea. The MVVM way to do validation is IDataErrorInfo, this is true and really you should be implementing the IDataErrorInfo interface to handle these errors.
On another note, here's what I did to get it working. I am handling the CollectionChanged event for the ObservableCollection of your VM's. When the collection changes, you need to find the element that was actually being removed, if found, we can try and clear it's ValidationError object for that element itself.
Here is the class -
public partial class MainWindow : Window
{
public ObservableCollection<VM> Items { get; } = new ObservableCollection<VM> { new VM(), new VM(), new VM() };
public MainWindow()
{
InitializeComponent();
Items.CollectionChanged += Items_CollectionChanged;
AddHandler(Validation.ErrorEvent, new RoutedEventHandler((s, e) =>
Title = ((ValidationErrorEventArgs)e).Action == ValidationErrorEventAction.Added ? "+" : "-"));
DataContext = this;
}
private void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) {
foreach (TextBox tb in FindVisualChildren<TextBox>(this))
{
if(tb.DataContext == e.OldItems[0])
{
Validation.ClearInvalid(tb.GetBindingExpression(TextBox.TextProperty));
break;
}
}
}
}
private void Button_Click(object sender, RoutedEventArgs e) => Items.RemoveAt(0);
public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
}
public class VM
{
public int Test { get; set; }
}
The bread and butter to make this work is Validation.ClearInvalid(tb.GetBindingExpression(TextBox.TextProperty)); which actually removes all ValidationError objects from the BindingExpressionBase object which in this case is TextBox.TextProperty.
Note: There has been no error checking here, you may want to do that.

The wpf controls dose't displaying on the canvas

I inheritance from a canvas control and I create my custom canvas class like this:
public class MyCanvas:Canvas
{
//this list will contains all shape
VisualCollection graphicsList;
List<GraphicsBase> cloneGraphicsList;
int c = 0;
double deltaX = 0;
double deltaY = 0;
public MyCanvas()
:base()
{
graphicsList = new VisualCollection(this);
cloneGraphicsList = new List<GraphicsBase>();
}
public VisualCollection GraphicsList
{
get
{
return graphicsList;
}
set
{
graphicsList = value;
}
}
protected override int VisualChildrenCount
{
get
{
return graphicsList.Count;
}
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= graphicsList.Count)
{
throw new ArgumentOutOfRangeException("index");
}
return graphicsList[index];
}
public GraphicsBase this[int index]
{
get
{
if (index >= 0 && index < GraphicsList.Count)
{
return (GraphicsBase)GraphicsList[index];
}
return null;
}
}
public int Count
{
get
{
return GraphicsList.Count;
}
}
}
and in XAML use this code:
<Window x:Class="MyNameSpace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:CustomCanvas="clr-namespace:MyNameSpace"
xmlns:WPFRuler="clr-namespace:Orbifold.WPFRuler;assembly=Orbifold.WPFRuler"
Title="PrintVarsDesigner" Height="709" Width="964"
Background="LightGray" Grid.IsSharedSizeScope="False" OverridesDefaultStyle="False"
WindowState="Maximized" WindowStartupLocation="CenterScreen">
<CustomCanvas:MyCanvas x:Name="myCanvas" Background="White" VerticalAlignment="Top"
Width="895" Height="1162">
</CustomCanvas:MyCanvas>
</Window>
the controls does't appear after add its from visual screen or C# code by add child to canvas.
Thanks for any advice.
Inheriting WPF controls is problematic to say the least. WPF control's are "lookless". that is to say, the control itself doesn't know how it's going to be presented. When the control is placed in a window WPF looks for the corresponding ControlTemplate to this specific control.
The problem with inherited controls is that they have no such template. If you want to display it, you'll have to write one yourself, and that's not always simple. You can find an example here, but i'd recommend against it. Use UserControls instead.

Get the shape of a control as Geometry

Is there a way to get the shape of a control and convert it into a Geometry object?
I have the following situation: In a WPF application a popup will be shown. This popup is no windows, it's a control, that will get visible. The rest of the application will get darker as a gray layer is above the application.
Now the problem is that this gray layer is also above the popup itself which is caused by the design of the application plus the element that was clicked and opened the popup should also not be hidden by the layer. I decided to attach a clipping geometry to the gray layer which is fine, but I have to detect all forms and paths that I don't want to hide by myself.
So to get back to my question: Is there a way to get the shape of a control and convert it into a Geometry object? E.g. I found ways to get the VisualBrush of a control but also cannot convert that - or just do not see how it is possible.
you could do it this way:
Remove the button from the visual tree and place it on the adorner.
When the adorner closes attach it to the original parent again.
I think this is much more flexible than clipping any geometries and makes it much more flexible (you could e.g. place complex content like usercontrols on the adorner)
The following example uses a Panel as container for the button.
The Xaml (Window):
<Grid Margin="50" x:Name="myGrid" Background="LightBlue">
<Button x:Name="myButton" Width="80" Height="30" Click="myButton_Click">Show popup</Button>
Code Behind:
private FrameworkElementAdorner _adorner;
private Panel _originalParent;
private void myButton_Click(object sender, RoutedEventArgs e)
{
if (_adorner == null)
{
_adorner = new FrameworkElementAdorner(myGrid);
}
// remove the button from the parent panel and attach it to the adorner
// otherwise remove from adorner and attach to original parent again
if (_adorner.IsVisible)
{
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(myGrid);
adornerLayer.Remove(_adorner);
Panel parent = VisualTreeHelper.GetParent(myButton) as Panel;
if (parent != null)
{
parent.Children.Remove(myButton);
}
_originalParent.Children.Add(myButton);
}
else
{
_originalParent = VisualTreeHelper.GetParent(myButton) as Panel;
if (_originalParent != null)
{
_originalParent.Children.Remove(myButton);
}
// Create the Adorner with the original button in it
_adorner.Child = CreateAdornerContent(myButton);
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(myGrid);
adornerLayer.Add(_adorner);
}
}
/// <summary>
/// Creates some dummy content for the adorner
/// </summary>
private FrameworkElement CreateAdornerContent(Button myButton)
{
Grid g = new Grid();
g.Background = new SolidColorBrush(Colors.Yellow);
TextBlock tb = new TextBlock();
tb.Text = "I am the Adorner";
g.Children.Add(tb);
g.Children.Add(myButton);
return g;
}
And here the simple adorner which just displays a frameworkElement:
class FrameworkElementAdorner : Adorner
{
private FrameworkElement _child;
public FrameworkElementAdorner(UIElement adornedElement)
: base(adornedElement)
{
}
protected override int VisualChildrenCount
{
get
{
return 1;
}
}
protected override Visual GetVisualChild(int index)
{
if (index != 0) throw new ArgumentOutOfRangeException();
return _child;
}
public FrameworkElement Child
{
get
{
return _child;
}
set
{
if (_child != null)
{
RemoveVisualChild(_child);
}
_child = value;
if (_child != null)
{
AddVisualChild(_child);
}
}
}
protected override Size ArrangeOverride(Size finalSize)
{
_child.Arrange(new Rect(new Point(0, 0), finalSize));
return new Size(_child.ActualWidth, _child.ActualHeight);
}
}
I can also upload the full sln if you like. Is this possible here somehow?

Using ListBox to navigate a TextBox with MVVM

I am trying to move the following function listView_SelectionChanged away from code-behind and handle it directly inside my ViewModel (or directly as XAML). And I was hoping that someone might have a better idea on how to implement this.
The TextBox contains Sections e.g. [Secion1] and to help navigate I have a ListBox on the side of the TextBox that contains a list of all Sections. If you click on one of the Sections it will automatically jump to that part of the Text.
The code currently looks something like this:
XAML
ListBox ItemsSource="{Binding Path=Sections}" Name="listBox"
SelectionMode="Single" Width="170"
DisplayMemberPath="Section"
SelectionChanged="listView_SelectionChanged"/>
<TextBox Name="TextBox1" Text="{Binding Path=Source}"/>
Model
public class SourceData
{
public SourceData()
{
Sections = new List<SectionData>();
}
public String Source { get; set; }
public List<SectionData> Sections { get; set; }
}
public class SectionData
{
public int Line { get; set; } // Line of the Section
public String Section { get; set; } // Section name (e.g. [Section1]
}
Code-behind
private void listView_SelectionChanged(object sender,
System.Windows.Controls.SelectionChangedEventArgs e)
{
var test = (SectionData)listBox.SelectedItem; // Get Information on the Section
if (test.Line > 0 && test.Line <= TextBox1.LineCount) // Validate
{
TextBox1.ScrollToLine(test.Line - 1); // Scroll to Line
}
}
In such situations I usually create an attached behavior (in your case it will be a behavior which will allow synchronizing textbox scrolled line), add property in ViewModel (SourceData) which will rule that attached behavior and bind behavior to this property.
Steps you should do in your case (I assume you know how to create an attached properties):
1) Create attached behavior ScrolledLine for textbox. It should support at least one-way binding. In attached property callback you will scroll textBox (to which behavior is attached) to the line. Below you will find a quick sample how to implement such a behavior.
2) Your SourceData should be extended with at least two properties: SelectedSection and ScrolledLine. ScrolledLine should be raising PropertyChanged. SelectedSection setter should change ScrolledLine:
private SectionData _selectedSection;
public SectionData SelectedSection
{
get { return _selectedSection; }
set
{
_selectedSection = value;
if (_selectedSection != null) SelectedLine = _selectedSection.Line;
}
}
3) Bind your view to these two new properties:
b below is xml-namespace for your attached behavior from #1
<ListBox ItemsSource="{Binding Path=Sections}" SelectionMode="Single" Width="170" DisplayMemberPath="Section" SelectedItem="{Binding SelectedSection, Mode=TwoWay}" />
<TextBox Text="{Binding Path=Source}" b:Behaviors.ScrolledLine="{Binding ScrolledLine}" />
4) Remove your listView_SelectionChanged event handler from view. Your view should not have any code except InitializeComponent from now on.
P.S.: Here is a sample how your attached behavior should look like:
public class b:Behaviors
{
#region Attached DP registration
public static int GetScrolledLine(TextBox obj)
{
return (int)obj.GetValue(ScrolledLineProperty);
}
public static void SetScrolledLine(TextBox obj, int value)
{
obj.SetValue(ScrolledLineProperty, value);
}
#endregion
public static readonly DependencyProperty ScrolledLineProperty=
DependencyProperty.RegisterAttached("ScrolledLine", typeof(int), typeof(Behaviors), new PropertyMetadata(ScrolledLine_Callback));
// This callback will be invoked when 'ScrolledLine' property will be changed. Here you should scroll a textbox
private static void ScrolledLine_Callback(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
var textbox = (TextBox) source;
int newLineValue = (int)e.NewValue;
if (newLineValue > 0 && newLineValue <= textBox.LineCount) // Validate
textbox.ScrollToLine(newLineValue - 1); // Scroll to Line
}
}

Categories