I have a canvas in one of my controls that I'd like to add/remove children to as a collection grows/shrinks.
I know that I can do this:
<Canvas>
<Canvas.Children>
<Rectangle/>
<TextBox/>
</Canvas.Children>
</Canvas>
But these elements are statically defined. I envision something like this:
<Canvas>
<Canvas.Children ItemsSource={Binding Path="ItemCollection", Converter="{StaticResource VisualConverter}}/>
</Canvas>
Is there something analogous to my above code, which actually works? I am aware of others posts, such as this one and this one but a problem I see with these answers (for my purposes) is that you lose the capability of naming the canvas and accessing it in code behind: it's an element in a template, and is therefore out of scope.
When it comes to showing a collection of items (and support Binding), you should think about ItemsControl. In this case you can just set its ItemsPanel to some ItemsPanelTemplate holding a Canvas, something like this:
<ItemsControl ItemsSource="{Binding ItemCollection,
Converter={StaticResource VisualConverter}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Now the ItemsControl acts like a Canvas holding some dynamic collection of children.
Update:
I doubt that you are following a wrong approach. Not sure how necessary those methods are so that you have to create a custom Canvas and require some easy access to it in codebehind. Using the standard Canvas would not be able to set some Binding for the Children property because it's readonly. Here I introduce a simple implementation for an attached property supporting binding instead of the standard non-attached Children property:
public static class CanvasService
{
public static readonly DependencyProperty ChildrenProperty = DependencyProperty.RegisterAttached("Children", typeof(IEnumerable<UIElement>), typeof(CanvasService), new UIPropertyMetadata(childrenChanged));
private static Dictionary<INotifyCollectionChanged, Canvas> references = new Dictionary<INotifyCollectionChanged, Canvas>();
public static IEnumerable<UIElement> GetChildren(Canvas cv)
{
return cv.GetValue(ChildrenProperty) as IEnumerable<UIElement>;
}
public static void SetChildren(Canvas cv, IEnumerable<UIElement> children)
{
cv.SetValue(ChildrenProperty, children);
}
private static void childrenChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
var canvas = target as Canvas;
repopulateChildren(canvas);
var be = canvas.GetBindingExpression(ChildrenProperty);
if (be != null)
{
var elements = (be.ResolvedSourcePropertyName == null ? be.ResolvedSource : be.ResolvedSource.GetType().GetProperty(be.ResolvedSourcePropertyName).GetValue(be.ResolvedSource)) as INotifyCollectionChanged;
if (elements != null)
{
var cv = references.FirstOrDefault(i => i.Value == canvas);
if (!cv.Equals(default(KeyValuePair<INotifyCollectionChanged,Canvas>)))
references.Remove(cv.Key);
references[elements] = canvas;
elements.CollectionChanged -= collectionChangedHandler;
elements.CollectionChanged += collectionChangedHandler;
}
} else references.Clear();
}
private static void collectionChangedHandler(object sender, NotifyCollectionChangedEventArgs e)
{
Canvas cv;
if (references.TryGetValue(sender as INotifyCollectionChanged, out cv))
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var item in e.NewItems) cv.Children.Add(item as UIElement);
break;
case NotifyCollectionChangedAction.Remove:
foreach (var item in e.OldItems) cv.Children.Remove(item as UIElement);
break;
case NotifyCollectionChangedAction.Reset:
repopulateChildren(cv);
break;
}
}
}
private static void repopulateChildren(Canvas cv)
{
cv.Children.Clear();
var elements = GetChildren(cv);
foreach (UIElement elem in elements){
cv.Children.Add(elem);
}
}
}
Usage in XAML:
<Canvas local:CanvasService.Children="{Binding ItemCollection,
Converter={StaticResource VisualConverter}}"/>
Again I think you should consider for another approach. You should have some solution around the ItemsControl.
Related
In short: is it correct in MVVM pattern to access main window datacontext and update it through behavior class?
long: I'm trying to learn WPF MVVM and make app where one of the functionalities is canvas with draggable ellipses. I found few examples of behaviors that could provide this functionality but they relied on TranslateTransform and this was not the solution I wanted. I want to extract the ellipse coordinates for furhter use.
I also use ItemsControl to display canvas and related items which made impossible to use Canvas.SetTop() command.
After several tries I found a working solution but I’m not sure if this is correct according to MVVM pattern. And if this is the simplest way to achieve the goal… I take up coding as a hobby
if I made some concept mistakes please let me know.
Short app description:
On app startup the instance of TestWindow2VM class is crated and assigned to main window as datacontext
TestWindow2VM class contains ObservableCollection which contains EllipseVM class.
EllipseVM class holds X,Y coordinates and some other data (brushes etc).
In XAML in ItemsControl the binding of ItemsSource is set to my ObservableCollection. In ItemsControl Datatemplate I bind ellipse properties to data stored in EllipseVM class and also add reference to my behavior class
in ItemsControl ItemContainerStyle canvas top and left properties are bound to my ObservableCollection
when ellipse is clicked my behavior class access the datacontext, finds the instance of EllipseVM class and changes X and Y coordinates basing on mouse cursor position relative to canvas.
Code below:
behavior:
public class CanvasDragBehavior
{
private Point _mouseCurrentPos;
private Point _mouseStartOffset;
private bool _dragged;
private static CanvasDragBehavior _dragBehavior = new CanvasDragBehavior();
public static CanvasDragBehavior dragBehavior
{
get { return _dragBehavior; }
set { _dragBehavior = value; }
}
public static readonly DependencyProperty IsDragProperty =
DependencyProperty.RegisterAttached("CanBeDragged",
typeof(bool), typeof(DragBehavior),
new PropertyMetadata(false, OnDragChanged));
public static bool GetCanBeDragged(DependencyObject obj)
{
return (bool)obj.GetValue(IsDragProperty);
}
public static void SetCanBeDragged(DependencyObject obj, bool value)
{
obj.SetValue(IsDragProperty, value);
}
private static void OnDragChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var element = (UIElement)sender;
var isDrag = (bool)(e.NewValue);
dragBehavior = new CanvasDragBehavior();
if (isDrag)
{
element.MouseLeftButtonDown += dragBehavior.ElementOnMouseLeftButtonDown;
element.MouseLeftButtonUp += dragBehavior.ElementOnMouseLeftButtonUp;
element.MouseMove += dragBehavior.ElementOnMouseMove;
}
else
{
element.MouseLeftButtonDown -= dragBehavior.ElementOnMouseLeftButtonDown;
element.MouseLeftButtonUp -= dragBehavior.ElementOnMouseLeftButtonUp;
element.MouseMove -= dragBehavior.ElementOnMouseMove;
}
}
private void ElementOnMouseMove(object sender, MouseEventArgs e)
{
if (!_dragged) return;
Canvas canvas = Extension.FindAncestor<Canvas>(((FrameworkElement)sender));
if (canvas != null)
{
_mouseCurrentPos = e.GetPosition(canvas);
FrameworkElement fe = (FrameworkElement)sender;
if (fe.DataContext.GetType() == typeof(EllipseVM))
{
// EllipseVM class contains X and Y coordinates that are used in ItemsControl to display the ellipse
EllipseVM ellipseVM = (EllipseVM)fe.DataContext;
double positionLeft = _mouseCurrentPos.X - _mouseStartOffset.X;
double positionTop = _mouseCurrentPos.Y - _mouseStartOffset.Y;
#region canvas border check
if (positionLeft < 0) positionLeft = 0;
if (positionTop < 0) positionTop = 0;
if (positionLeft > canvas.ActualWidth) positionLeft = canvas.ActualWidth-fe.Width;
if (positionTop > canvas.ActualHeight) positionTop = canvas.ActualHeight-fe.Height;
#endregion
ellipseVM.left = positionLeft;
ellipseVM.top = positionTop;
}
}
}
private void ElementOnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_mouseStartOffset = e.GetPosition((FrameworkElement)sender);
_dragged = true;
((UIElement)sender).CaptureMouse();
}
private void ElementOnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_dragged = false;
((UIElement)sender).ReleaseMouseCapture();
}
XAML:
<ItemsControl ItemsSource="{Binding scrollViewElements}" >
<ItemsControl.Resources>
<!--some other data templates here-->
</DataTemplate>
<DataTemplate DataType="{x:Type VM:EllipseVM}" >
<Ellipse Width="{Binding width}"
Height="{Binding height}"
Fill="{Binding fillBrush}"
Stroke="Red" StrokeThickness="1"
behaviors:CanvasDragBehavior.CanBeDragged="True"
/>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Transparent" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding top}"/>
<Setter Property="Canvas.Left" Value="{Binding left}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
MVVM distinct 3 kinds of object:
View
VModel
Model
The property of the view should be bound to the VModel, you try correctly to bind the view with EllipseVM, like a real Expert!
The issue on your project is that you have not a single View bound to a single VM, but you want an infinite number of VModels.
I will enumerate below some points of reflection:
I would like to challenge the fact that you register on different drag events:
element.MouseLeftButtonDown
you should register only when objects are created or destroyed.
CanvasDragBehavior : why do you implement a singleton like pattern with a static public property (without private constructor) ?
Avoid registering properties via string like "CanBeDragged" find a way to define and use interfaces eg IMoveFeature{bool IsDraggable}
Your code is too complicated and has some errors.
For example the static instance property of the CanvasDragBehavior is not required. It looks like you confused something here.
To position the element on the Canvas simply use the attached properties Canvas.Top and Canvas.Left.
Prefer the tunneling version of input events, prefixed with Preview. For example listen to PreviewMouseMove instead of MouseMove.
Another important fix is to use the WeakEventManager to subscribe to the events of the attached elements. Otherwise you create a potential memory leak (depending on the lifetime of the event publisher and event listener). Always remember to follow the following pattern to avoid such memory leaks: when you subscribe to events, ensure that you will always unsubscribe too. If you have no control over the object lifetime, always follow the Weak Event pattern and use the WeakEventManager to observe events.
In your case: when an item is removed from the ItemsControl.ItemsSource, your behavior won't be able to detect this change in order to unsubscribe from the corresponding events.
The risk of a memory leak in your context is not high, but better be safe than sorry and stick to the safety pattern.
When implementing a control or behavior, try to avoid tight coupling to data types and implementation details. Make the control or behavior as generic as possible. For this reason, your behavior should not know about the DataContext and what type of elements are dragged. This way you can simply extend your code or reuse the behavior for example to allow to drag a Rectangle too. Right now, your code only works with a Ellipse or EllipseVM.
Usually, you don't need the position data in your view model. If it's pure UI drag&Drop the coordinates are part of the view only. In this case you would prefer to attach the behavior to the item container instead of attaching it to the elements of the DataTemplate: you don't want to drag the data model. You want to drag the item container.
If you still need the coordinates in your model, you would setup a binding in the ItemsControl.ItemContainerStyle like in the example below (the DataContext of the item container Style is always the data item, which is of type EllipseVM in your case).
The simplified and improved version that targets dragging the item container rather than the data model could look as follows. Note that the following behavior is implemented by only using the UIElement type for the dragged object. The actual type of the element or the data model is not required at all. This way it will work with every shape or control (not only Ellipse). You can even drag a Button or whatever is defined in the DataTemplate. The DataContext can be any type.
public class CanvasDragBehavior
{
public static readonly DependencyProperty IsDragEnabledProperty = DependencyProperty.RegisterAttached(
"IsDragEnabled",
typeof(bool),
typeof(CanvasDragBehavior),
new PropertyMetadata(false, OnIsDragEnabledChanged));
public static bool GetIsDragEnabled(DependencyObject obj) => (bool)obj.GetValue(IsDragEnabledProperty);
public static void SetIsDragEnabled(DependencyObject obj, bool value) => obj.SetValue(IsDragEnabledProperty, value);
private static Point DragStartPosition { get; set; }
private static ConditionalWeakTable<UIElement, FrameworkElement> ItemToItemHostMap { get; } = new ConditionalWeakTable<UIElement, FrameworkElement>();
private static void OnIsDragEnabledChanged(object attachingElement, DependencyPropertyChangedEventArgs e)
{
if (attachingElement is not UIElement uiElement)
{
return;
}
var isEnabled = (bool)e.NewValue;
if (isEnabled)
{
WeakEventManager<UIElement, MouseButtonEventArgs>.AddHandler(uiElement, nameof(uiElement.PreviewMouseLeftButtonDown), OnDraggablePreviewMouseLeftButtonDown);
WeakEventManager<UIElement, MouseEventArgs>.AddHandler(uiElement, nameof(uiElement.PreviewMouseMove), OnDraggablePreviewMouseMove);
WeakEventManager<UIElement, MouseButtonEventArgs>.AddHandler(uiElement, nameof(uiElement.PreviewMouseLeftButtonUp), OnDraggablePreviewMouseLeftButtonUp);
}
else
{
WeakEventManager<UIElement, MouseButtonEventArgs>.RemoveHandler(uiElement, nameof(uiElement.PreviewMouseLeftButtonDown), OnDraggablePreviewMouseLeftButtonDown);
WeakEventManager<UIElement, MouseEventArgs>.RemoveHandler(uiElement, nameof(uiElement.PreviewMouseMove), OnDraggablePreviewMouseMove);
WeakEventManager<UIElement, MouseButtonEventArgs>.RemoveHandler(uiElement, nameof(uiElement.PreviewMouseLeftButtonUp), OnDraggablePreviewMouseLeftButtonUp);
}
}
private static void OnDraggablePreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Released)
{
return;
}
var draggable = sender as UIElement;
if (!ItemToItemHostMap.TryGetValue(draggable, out FrameworkElement draggableHost))
{
return;
}
Point newDragEndPosition = e.GetPosition(draggableHost);
newDragEndPosition.Offset(-DragStartPosition.X, -DragStartPosition.Y);
Canvas.SetLeft(draggable, newDragEndPosition.X);
Canvas.SetTop(draggable, newDragEndPosition.Y);
}
private static void OnDraggablePreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var draggable = sender as UIElement;
if (!ItemToItemHostMap.TryGetValue(draggable, out _))
{
if (!TryGetVisualParent(draggable, out Panel draggableHost))
{
return;
}
ItemToItemHostMap.Add(draggable, draggableHost);
}
DragStartPosition = e.GetPosition(draggable);
draggable.CaptureMouse();
}
private static void OnDraggablePreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
=> (sender as UIElement)?.ReleaseMouseCapture();
private static bool TryGetVisualParent<TParent>(DependencyObject element, out TParent parent) where TParent : DependencyObject
{
parent = null;
if (element is null)
{
return false;
}
element = VisualTreeHelper.GetParent(element);
if (element is TParent parentElement)
{
parent = parentElement;
return true;
}
return TryGetVisualParent(element, out parent);
}
}
Usage example
DataItem.cs
class DataItem : INotifyPropertyChanged
{
// Allow this item to change its coordinates aka get dragged
private bool isPositionDynamic;
public bool IsPositionDynamic
{
get => this.isPositionDynamic;
set
{
this.isPositionDynamic = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
MainWindow.xaml
<Window>
<ItemsControl ItemsSource="{Binding DataItems}"
Height="1000"
Width="1000">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:DataItem}">
<Ellipse Width="50"
Height="50"
Fill="Red"
Stroke="Black"
StrokeThickness="1" />
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Width="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl}, Path=ActualHeight}"
Background="Gray" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<!-- If every item is draggable, simply set this property to 'True' -->
<Setter Property="local:CanvasDragBehavior.IsDragEnabled"
Value="{Binding IsPositionDynamic}" />
<!-- Optional binding if you need the coordinates in the view model.
This example assumes that the view model has a Top and Left property -->
<Setter Property="Canvas.Top"
Value="{Binding Top, Mode=TwoWay}" />
<Setter Property="Canvas.Left"
Value="{Binding Left, Mode=TwoWay}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Window>
I am trying to add a search feature to a TreeView in WPF. I was successfully able to add and search the tree items for a TreeView which has just one level, but for a TreeView with multiple levels, I am not able to figure out how to access the child controls (UIElement) of a TreeViewItem. ItemContainerGenerator.ContainerFromItem() returns null when I pass TreeViewItem.Item as an argument. I saw few posts which discussed the same issue, but none of the solutions worked for me.
Here's my XAML code.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="72*" />
</Grid.RowDefinitions>
<StackPanel x:Name="SearchTextBox" Grid.Row="0" Margin="5,5,5,0"/>
<TreeView Name="MyTreeView" Grid.Row="1" BorderThickness="0">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Students}">
<TextBlock Margin="3" Text="{Binding SchoolName}"/>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding StudentName}"/>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
Here's my C# code
public class MyClass
{
public string ClassName { get; set; }
public List<Student> Students { get; set; }
}
public class Student
{
public string StudentName { get; set; }
}
private void PlanSearchOnSearchInputChanged(string searchQuery)
{
List<TreeViewItem> items = GetLogicalChildCollection(MyTreeView);
}
public static List<TreeViewItem> GetLogicalChildCollection(TreeView parent)
{
var logicalCollection = new List<TreeViewItem>();
foreach (var child in parent.Items)
{
TreeViewItem item = (TreeViewItem)parent.ItemContainerGenerator.ContainerFromItem(child);
logicalCollection.Add(item);
GetLogicalChildCollection(item, logicalCollection);
}
return logicalCollection;
}
public static List<TreeViewItem> GetLogicalChildCollection(TreeViewItem parent, List<TreeViewItem> logicalCollection)
{
foreach (var child in parent.Items)
{
TreeViewItem item = (TreeViewItem)parent.ItemContainerGenerator.ContainerFromItem(child);
logicalCollection.Add(item);
}
return logicalCollection;
}
PS: I need access to UIElement of the item, not just item.
Any help will be greatly appreciated.
If the TreeViewItem is not expanded then its children will not have any UI so you can't get their children. Maybe that's the problem you're seeing.
This is some code to think about, I don't have all your code to easily work out what's happening but this works on a sample I have. It is quick and dirty rather than super elegant.
private void Button_Click(object sender, RoutedEventArgs e)
{
foreach (var item in tv.Items)
{
TreeViewItem tvi = tv.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
tvi.IsExpanded = true;
tvi.UpdateLayout();
RecurseItem(tvi);
}
}
private bool gotTheItem = false;
private void RecurseItem(TreeViewItem item)
{
foreach (var subItem in item.Items)
{
TreeViewItem tvi = item.ItemContainerGenerator.ContainerFromItem(subItem) as TreeViewItem;
// do something
if (!gotTheItem)
{
RecurseItem(tvi);
}
}
}
You could set IsExpanded back or hook the event fires when you expand an item and probably some other things.
The TV has a nodes collection. Each node also has a nodes collection.
You need to start at the root, examine each node, and if that node contains a nodes collection with a length > 0, you need to descend into it.
This is typically done with a recursive tree walk.
Well to display "dynamic" data an easy way is to use an ItemsControl (with, say, a WrapPanel as item template).
Now I wish for my application, a rich text box filled with runs is ideal. - The (number & data) of the runs depends on an observable collection in my viewmodel. If I would use a WrapPanel instead of a RichTextBox the itemscontrol code would look like:
<ItemsControl ItemsSource="{Binding Data}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True">
</WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Now I tried using a richtextbox in my usercontrol, the xaml for the usercontrol then looks like:
<UserControl x:Class="testit.MyControl"
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:testit"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:DispData}">
<TextBlock>
<Run Text="{Binding Text}"></Run>
</TextBlock>
</DataTemplate>
</UserControl.Resources>
<StackPanel>
<RichTextBox IsReadOnly="True" IsDocumentEnabled="True" VerticalScrollBarVisibility="Auto">
<FlowDocument>
<ItemsControl ItemsSource="{Binding Data}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Paragraph IsItemsHost="True">
</Paragraph>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</FlowDocument>
</RichTextBox>
<ItemsControl ItemsSource="{Binding Data}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True">
</WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>
</UserControl>
The viewmodel which is bound to the datacontext of the usercontrol is:
namespace testit
{
class ViewModel : INotifyPropertyChanged
{
private readonly ObservableCollection<DispData> _data =
new ObservableCollection<DispData>();
public ReadOnlyObservableCollection<DispData> Data { get; private set; }
public ViewModel() {
Data = new ReadOnlyObservableCollection<DispData>(_data);
_data.Add(new DispData("hello"));
_data.Add(new DispData("world"));
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
This gives a lot of errors with the RichTextBox (first error is that ItemsControl can't be placed there, but another is that Paragraph doesn't have the IsItemsHost property. -- I also wish to stress that if I comment out the richtextbox xaml, the xaml for the wrap panel does work: so it's not the bindings or anything that is wrong.
Can a RichTextBox even be used with an ItemsControl - and if not, how would I fill the content of the textbox in a MVVM fashion?
You should check out this article on how to write your own items control that is compatible with FlowDocument or RichTextBox. Code sample can be found at this location
Once you download the controls, update if-else condition in GenerateContent() method in ItemsContent as following to add support for Paragraph and Inlines.
private void GenerateContent(DataTemplate itemsPanel, DataTemplate itemTemplate, IEnumerable itemsSource)
{
....
if (panel is Section)
((Section)panel).Blocks.Add(Helpers.ConvertToBlock(data, element));
else if (panel is TableRowGroup)
((TableRowGroup)panel).Rows.Add((TableRow)element);
else if (panel is Paragraph && element is Inline)
((Paragraph)panel).Inlines.Add((Inline)element);
else
throw new Exception(String.Format("Don't know how to add an instance of {0} to an instance of {1}", element.GetType(), panel.GetType()));
And update your XAML to:
<RichTextBox IsReadOnly="True" IsDocumentEnabled="True" VerticalScrollBarVisibility="Auto">
<FlowDocument>
<flowdoc:ItemsContent ItemsSource ItemsSource="{Binding Data}">
<flowdoc:ItemsContent.ItemsPanel>
<DataTemplate>
<flowdoc:Fragment>
<Paragraph flowdoc:Attached.IsItemsHost="True" />
</flowdoc:Fragment>
</DataTemplate>
</flowdoc:ItemsContent.ItemsPanel>
<flowdoc:ItemsContent.ItemTemplate>
<DataTemplate>
<flowdoc:Fragment>
<flowdoc:BindableRun BoundText="{Binding Text}" />
</flowdoc:Fragment>
</DataTemplate>
</flowdoc:ItemsContent.ItemTemplate>
</flowdoc:ItemsContent>
</FlowDocument>
</RichTextBox>
EDIT - 1
As #ed-plunkett suggested, sharing relevant code here (in case the external link doesn't work)
In order to be able to use Run in item-template (similar to label or textblock); you will need to extend Run to add a bindable property.
public class BindableRun : Run
{
public static readonly DependencyProperty BoundTextProperty = DependencyProperty.Register("BoundText", typeof(string), typeof(BindableRun), new PropertyMetadata(OnBoundTextChanged));
public BindableRun()
{
Helpers.FixupDataContext(this);
}
private static void OnBoundTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((Run)d).Text = (string)e.NewValue;
}
public String BoundText
{
get { return (string)GetValue(BoundTextProperty); }
set { SetValue(BoundTextProperty, value); }
}
}
Next you would need is the ability to mark a container control as items host; that can be done by defining an attached property.
public class Attached
{
private static readonly DependencyProperty IsItemsHostProperty = DependencyProperty.RegisterAttached("IsItemsHost", typeof(bool), typeof(Attached), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.NotDataBindable, OnIsItemsHostChanged));
private static readonly DependencyProperty ItemsHostProperty = DependencyProperty.RegisterAttached("ItemsHost", typeof(FrameworkContentElement), typeof(Attached), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.NotDataBindable));
public static bool GetIsItemsHost(DependencyObject target)
{
return (bool)target.GetValue(IsItemsHostProperty);
}
public static void SetIsItemsHost(DependencyObject target, bool value)
{
target.SetValue(IsItemsHostProperty, value);
}
private static void SetItemsHost(FrameworkContentElement element)
{
FrameworkContentElement parent = element;
while (parent.Parent != null)
parent = (FrameworkContentElement)parent.Parent;
parent.SetValue(ItemsHostProperty, element);
}
public static FrameworkContentElement GetItemsHost(DependencyObject dp)
{
return (FrameworkContentElement)dp.GetValue(ItemsHostProperty);
}
private static void OnIsItemsHostChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
FrameworkContentElement element = (FrameworkContentElement)d;
if (element.IsInitialized)
SetItemsHost(element);
else
element.Initialized += ItemsHost_Initialized;
}
}
private static void ItemsHost_Initialized(object sender, EventArgs e)
{
FrameworkContentElement element = (FrameworkContentElement)sender;
element.Initialized -= ItemsHost_Initialized;
SetItemsHost(element);
}
}
A fragment control that you can use to embed FrameworkContentElement inside DataTemplate.
[ContentProperty("Content")]
public class Fragment : FrameworkElement
{
private static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(FrameworkContentElement), typeof(Fragment));
public FrameworkContentElement Content
{
get
{
return (FrameworkContentElement)GetValue(ContentProperty);
}
set
{
SetValue(ContentProperty, value);
}
}
}
And, finally the items control itself:, that does the major heavy lifting:
public class ItemsContent : Section
{
private static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(ItemsContent), new PropertyMetadata(OnItemsSourceChanged));
private static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(ItemsContent), new PropertyMetadata(OnItemTemplateChanged));
private static readonly DependencyProperty ItemsPanelProperty = DependencyProperty.Register("ItemsPanel", typeof(DataTemplate), typeof(ItemsContent), new PropertyMetadata(OnItemsPanelChanged));
public ItemsContent()
{
Helpers.FixupDataContext(this);
Loaded += ItemsContent_Loaded;
}
private void ItemsContent_Loaded(object sender, RoutedEventArgs e)
{
GenerateContent(ItemsPanel, ItemTemplate, ItemsSource);
}
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public DataTemplate ItemsPanel
{
get { return (DataTemplate)GetValue(ItemsPanelProperty); }
set { SetValue(ItemsPanelProperty, value); }
}
private void GenerateContent(DataTemplate itemsPanel, DataTemplate itemTemplate, IEnumerable itemsSource)
{
Blocks.Clear();
if (itemTemplate != null && itemsSource != null)
{
FrameworkContentElement panel = null;
foreach (object data in itemsSource)
{
if (panel == null)
{
if (itemsPanel == null)
panel = this;
else
{
FrameworkContentElement p = Helpers.LoadDataTemplate(itemsPanel);
if (!(p is Block))
throw new Exception("ItemsPanel must be a block element");
Blocks.Add((Block)p);
panel = Attached.GetItemsHost(p);
if (panel == null)
throw new Exception("ItemsHost not found. Did you forget to specify Attached.IsItemsHost?");
}
}
FrameworkContentElement element = Helpers.LoadDataTemplate(itemTemplate);
element.DataContext = data;
Helpers.UnFixupDataContext(element);
if (panel is Section)
((Section)panel).Blocks.Add(Helpers.ConvertToBlock(data, element));
else if (panel is TableRowGroup)
((TableRowGroup)panel).Rows.Add((TableRow)element);
else if (panel is Paragraph && element is Inline)
((Paragraph)panel).Inlines.Add((Inline)element);
else
throw new Exception(String.Format("Don't know how to add an instance of {0} to an instance of {1}", element.GetType(), panel.GetType()));
}
}
}
private void GenerateContent()
{
GenerateContent(ItemsPanel, ItemTemplate, ItemsSource);
}
private void OnItemsSourceChanged(IEnumerable newValue)
{
if (IsLoaded)
GenerateContent(ItemsPanel, ItemTemplate, newValue);
}
private void OnItemTemplateChanged(DataTemplate newValue)
{
if (IsLoaded)
GenerateContent(ItemsPanel, newValue, ItemsSource);
}
private void OnItemsPanelChanged(DataTemplate newValue)
{
if (IsLoaded)
GenerateContent(newValue, ItemTemplate, ItemsSource);
}
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ItemsContent)d).OnItemsSourceChanged((IEnumerable)e.NewValue);
}
private static void OnItemTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ItemsContent)d).OnItemTemplateChanged((DataTemplate)e.NewValue);
}
private static void OnItemsPanelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ItemsContent)d).OnItemsPanelChanged((DataTemplate)e.NewValue);
}
}
Static helper methods:
internal static class Helpers
{
/// <summary>
/// If you use a bindable flow document element more than once, you may encounter a "Collection was modified" exception.
/// The error occurs when the binding is updated because of a change to an inherited dependency property. The most common scenario
/// is when the inherited DataContext changes. It appears that an inherited properly like DataContext is propagated to its descendants.
/// When the enumeration of descendants gets to a BindableXXX, the dependency properties of that element change according to the new
/// DataContext, which change the (non-dependency) properties. However, for some reason, changing the flow content invalidates the
/// enumeration and raises an exception.
/// To work around this, one can either DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType=FrameworkElement}}"
/// in code. This is clumsy, so every derived type calls this function instead (which performs the same thing).
/// See http://code.logos.com/blog/2008/01/data_binding_in_a_flowdocument.html
/// </summary>
/// <param name="element"></param>
public static void FixupDataContext(FrameworkContentElement element)
{
Binding b = new Binding(FrameworkContentElement.DataContextProperty.Name);
// another approach (if this one has problems) is to bind to an ancestor by ElementName
b.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(FrameworkElement), 1);
element.SetBinding(FrameworkContentElement.DataContextProperty, b);
}
private static bool InternalUnFixupDataContext(DependencyObject dp)
{
// only consider those elements for which we've called FixupDataContext(): they all belong to this namespace
if (dp is FrameworkContentElement && dp.GetType().Namespace == typeof(Helpers).Namespace)
{
Binding binding = BindingOperations.GetBinding(dp, FrameworkContentElement.DataContextProperty);
if (binding != null
&& binding.Path != null && binding.Path.Path == FrameworkContentElement.DataContextProperty.Name
&& binding.RelativeSource != null && binding.RelativeSource.Mode == RelativeSourceMode.FindAncestor && binding.RelativeSource.AncestorType == typeof(FrameworkElement) && binding.RelativeSource.AncestorLevel == 1)
{
BindingOperations.ClearBinding(dp, FrameworkContentElement.DataContextProperty);
return true;
}
}
// as soon as we have disconnected a binding, return. Don't continue the enumeration, since the collection may have changed
foreach (object child in LogicalTreeHelper.GetChildren(dp))
if (child is DependencyObject)
if (InternalUnFixupDataContext((DependencyObject)child))
return true;
return false;
}
public static void UnFixupDataContext(DependencyObject dp)
{
while (InternalUnFixupDataContext(dp))
;
}
/// <summary>
/// Convert "data" to a flow document block object. If data is already a block, the return value is data recast.
/// </summary>
/// <param name="dataContext">only used when bindable content needs to be created</param>
/// <param name="data"></param>
/// <returns></returns>
public static Block ConvertToBlock(object dataContext, object data)
{
if (data is Block)
return (Block)data;
else if (data is Inline)
return new Paragraph((Inline)data);
else if (data is BindingBase)
{
BindableRun run = new BindableRun();
if (dataContext is BindingBase)
run.SetBinding(BindableRun.DataContextProperty, (BindingBase)dataContext);
else
run.DataContext = dataContext;
run.SetBinding(BindableRun.BoundTextProperty, (BindingBase)data);
return new Paragraph(run);
}
else
{
Run run = new Run();
run.Text = (data == null) ? String.Empty : data.ToString();
return new Paragraph(run);
}
}
public static FrameworkContentElement LoadDataTemplate(DataTemplate dataTemplate)
{
object content = dataTemplate.LoadContent();
if (content is Fragment)
return (FrameworkContentElement)((Fragment)content).Content;
else if (content is TextBlock)
{
InlineCollection inlines = ((TextBlock)content).Inlines;
if (inlines.Count == 1)
return inlines.FirstInline;
else
{
Paragraph paragraph = new Paragraph();
// we can't use an enumerator, since adding an inline removes it from its collection
while (inlines.FirstInline != null)
paragraph.Inlines.Add(inlines.FirstInline);
return paragraph;
}
}
else
throw new Exception("Data template needs to contain a <Fragment> or <TextBlock>");
}
}
I want to add a child item in a TreeViewItem that was added previously. The problem with code like this:
How to insert a child node in a TreeView Control in WPF?
or with many other variations that I have tried to use like:
for (int i = 1; i <= dataTreeview.Items.Count; i++)
{
TreeViewItem tempTVI = (TreeViewItem)dataTreeview.Items.GetItemAt(i);
}
is that I get an InvalidCastException exception because items(in the other stackoverflow question) or tempTVI are strings not TreeViewItem
I don't know why this is and I ran out of ideas.
If it helps I am using Visual Studio 2015 Community in preview.
Thank you for your help.
The TreeView in WPF is an extension of ItemsControl. There are basically two ways to work with these controls, one that makes dynamically changing trees easy to manage, and one that makes completely static trees trivial to setup.
Dynamic Trees
The way TreeView was designed to be used is following the MVVM design pattern. Here is a quick example of this.
First, when working with MVVM, you always want some sort of base class for viewmodels that implements property change notification. Here is the most basic example:
internal class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Then, you need a class that represents the data for a single node in the tree. For example:
internal class Node : ObservableObject
{
private ObservableCollection<Node> mChildren;
// Add all of the properties of a node here. In this example,
// all we have is a name and whether we are expanded.
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
NotifyPropertyChanged();
}
}
}
private string _name;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (_isExpanded != value)
{
_isExpanded = value;
NotifyPropertyChanged();
}
}
}
private bool _isExpanded;
// Children are required to use this in a TreeView
public IList<Node> Children { get { return mChildren; } }
// Parent is optional. Include if you need to climb the tree
// from code. Not usually necessary.
public Node Parent { get; private set; }
public Node(Node parent = null)
{
mChildren = new ObservableCollection<Node>();
IsExpanded = true;
Parent = parent;
}
}
Now, create a viewmodel for your control with a collection of these nodes in it. In this example, the viewmodel is for the main window of the application:
internal class MainWindowVM : ObservableObject
{
private ObservableCollection<Node> mRootNodes;
public IEnumerable<Node> RootNodes { get { return mRootNodes; } }
public MainWindowVM()
{
mRootNodes = new ObservableCollection<Node>();
// Test data for example purposes
Node root = new Node() { Name = "Root" };
Node a = new Node(root) { Name = "Node A" };
root.Children.Add(a);
Node b = new Node(root) { Name = "Node B" };
root.Children.Add(b);
Node c = new Node(b) { Name = "Node C" };
b.Children.Add(c);
Node d = new Node(b) { Name = "Node D" };
b.Children.Add(d);
Node e = new Node(root) { Name = "Node E" };
root.Children.Add(e);
mRootNodes.Add(root);
}
}
Finally, create the TreeView instance and set it up to use your data. In this example, the TreeView is the only thing in the main application window:
<Window x:Class="WpfTreeViewExample.MainWindow"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfTreeViewExample"
SizeToContent="WidthAndHeight"
Title="MainWindow">
<Window.DataContext>
<local:MainWindowVM />
</Window.DataContext>
<TreeView
Margin="10"
ItemsSource="{Binding RootNodes}">
<ItemsControl.ItemContainerStyle>
<Style
TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded}" />
<!-- Could also put IsSelected here if we needed it in our Node class -->
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<HierarchicalDataTemplate
DataType="{x:Type local:Node}"
ItemsSource="{Binding Children}">
<!-- Can build any view we want here to be used for each node -->
<!-- Simply displaying the name in a text block for this example -->
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</ItemsControl.ItemTemplate>
</TreeView>
</Window>
Once you have this setup, all you have to do from now on is manipulate the data in your viewmodel, and the TreeView will automatically update to reflect those changes. This way, you never need to manipulate the control directly.
Here is the resulting view:
Static Trees
If the entire tree is known ahead of time and will never change, you can set it up pretty simply like this:
<TreeView
x:Name="mTreeView"
Margin="10">
<TreeViewItem Header="Root">
<TreeViewItem Header="Node A" />
<TreeViewItem Header="Node B">
<TreeViewItem Header="Node C" />
<TreeViewItem Header="Node D" />
</TreeViewItem>
<TreeViewItem Header="Node E" />
</TreeViewItem>
</TreeView>
The problem with this approach is that when you want to modify the tree programmatically, it becomes hard to manage because you have to deal with the TreeView itself. However, it is doable from a code-behind. For example, if I want to add a new child node under "Node C" called "New Node", I could do something like this:
((TreeViewItem)((TreeViewItem)((TreeViewItem)mTreeView.Items[0]).Items[1]).Items[0]).Items.Add(new TreeViewItem() { Header = "New Node" });
Working this way gets messy though. Since we don't have a parallel representation of the tree in data, we have to keep accessing things through the control and casting them.
Some Other Setup
Looking at your question, it looks like you are not following either of these approaches, but instead have a TreeView that is setup basically like this:
<TreeView>
<sys:String>Node A</sys:String>
<sys:String>Node B</sys:String>
</TreeView>
So, you have a TreeView full of strings. Internally, an ItemsControl can take any object and wrap it in an item container. TreeView will wrap those strings in TreeViewItem instances. However, the items are still stored as strings, and accessing TreeView.Items will return the strings that you added.
Getting the TreeViewItem associated with an arbitrary item in a TreeView is actually fairly difficult because you have to get the containers for each item at the root level, then dig into each of those and get containers for their items, and so on all the way through the tree until you find the item you are looking for.
You can find an example for how to lookup item containers here. Note that you cannot use virtualization in your TreeView in order for this to work reliably. Also, I would recommend against working this way because you are making things harder on yourself.
You are getting String because that is what the source of the TreeView must have been bound to.
Using this method allows you to iterate over the items and retrieve the TreeViewItem containers that they are inside:
List<TreeViewItem> GetChildren(TreeViewItem parent)
{
List<TreeViewItem> children = new List<TreeViewItem>();
if (parent != null)
{
foreach (var item in parent.Items)
{
TreeViewItem child = item as TreeViewItem;
if (child == null)
{
child = parent.ItemContainerGenerator.ContainerFromItem(child) as TreeViewItem;
}
children.Add(child);
}
}
return children;
}
Please notice that they check if the TreeViewItem is null after casting it. This is good practice as it prevents the null reference exception from crashing your application if something does happen to go wrong.
source: https://social.msdn.microsoft.com/Forums/vstudio/en-US/595f0c84-01e7-4534-b73b-704b41713fd5/traversing-the-children-of-a-treeviewitem
Maybe you forgot the tags?
If you have the following in your xaml file:
<TreeView x:Name="MyTreeView">
<TreeViewItem>Hello</TreeViewItem>
World
</TreeView>
And the following in the code-behind:
var a = MyTreeView.Items.GetItemAt(0) as string;
var b = MyTreeView.Items.GetItemAt(0) as TreeViewItem;
var c = MyTreeView.Items.GetItemAt(1) as string;
var d = MyTreeView.Items.GetItemAt(1) as TreeViewItem;
Variables a and d will be null whereas b will be a TreeViewItem and c will be a string.
In my windows phone 8 application I've created truly observable collection for noticing when item changes in collection. Here is collection code:
public class TrulyObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public TrulyObservableCollection()
: base()
{
CollectionChanged += new NotifyCollectionChangedEventHandler(TrulyObservableCollection_CollectionChanged);
}
void TrulyObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (Object item in e.NewItems)
{
var test = item as INotifyPropertyChanged;
(item as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}
}
if (e.OldItems != null)
{
foreach (Object item in e.OldItems)
{
(item as INotifyPropertyChanged).PropertyChanged -= new PropertyChangedEventHandler(item_PropertyChanged);
}
}
}
void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
NotifyCollectionChangedEventArgs a = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
OnCollectionChanged(a);
}
}
The situation which im going to describe happens for both controls: expander view (silverlight toolkit) and expander control (telerik controls).
In my view I have list of expander views/controls. On Item tap event I would like to change something in tapped item and refresh list. Here is the code how i'm doing it:
ViewModel:
public TrulyObservableCollection<SymbolRecord> Symbols {get; set;}
private RelayCommand<SymbolRecord> tapCommand;
public RelayCommand<SymbolRecord> TapCommand
{
get
{
return tapCommand ?? (tapCommand = new RelayCommand<SymbolRecord>((item) =>
{
item.Symbol = "test";
}));
}
}
View:
<telerikData:RadJumpList x:Name="ListControl" Grid.Row="1"IsCheckModeEnabled="False" GroupPickerItemTemplate="{StaticResource JumpListHeaderItemTemplate}" GroupHeaderTemplate="{StaticResource JumpListHeaderTemplate}" ItemsSource="{Binding Path=Symbols}" >
<telerikData:RadJumpList.ItemTemplate>
<DataTemplate>
<StackPanel>
<toolkit:ExpanderView ItemsSource="{Binding}" Expander="{Binding}" VerticalAlignment="Stretch" VerticalContentAlignment="Stretch" ExpanderTemplate="{StaticResource ExpanderControlContentTemplate}" >
<toolkit:ExpanderView.Items> <TextBox Text="TEST"></TextBox></toolkit:ExpanderView.Items>
</toolkit:ExpanderView>
</StackPanel>
</DataTemplate>
</telerikData:RadJumpList.ItemTemplate>
</telerikData:RadJumpList>
<i:Interaction.Triggers>
<i:EventTrigger EventName="ItemTap" SourceName="ListControl" >
<cmd:EventToCommand Command="{Binding TapCommand}" CommandParameter="{Binding SelectedItem,
ElementName=ListControl}" />
</i:EventTrigger>
</i:Interaction.Triggers>
When I click on item in emulator the property Symbol is changing but what's more random items are being expanded. Dont know why this is happening. Please help me
What do you mean by refreshing your list?
Do you want to put some new data int it? If so there's no need to use TrulyObser... nor call CollectionChanged at all. Simply use methods like remove, add, insert etc.
Item template automatically get's as datacontext proper collection element. And binding works normal. Simple INotifyCollectionChanged will be enough.
If you want to expand an expander do it in view's code-behind. If code-behind is something that you want to avoid create property IsExpanded in VM and bind it to Expander's IsExpanded property.
You can set your's VM IsExpanded property using that TrulyObservableCollection. When one of item's in the collection changed set it's IsExpanded to true.
Anyways. In most cases there's no need for "TrulyObservableCollection"