I have a usercontrol which I want to animate via rendertransform. (rot, trans, scale)
It works out fine, but the control is on a layer below layers with other controls, which are created by itemcontrols with itemsources.
The usercontrol should not know about it's parents so I guess i'm looking for something like a universal render zindex.
My idea was to use a popup but it comes with peformance disadvantages and mouse event complications.
(that was with a bitmapcachebrush )
I guess i cant take out the item from the itemssource of the itemscontrol and put it on a front layer.
Is an adorner what I am looking for? Also it doesnt have to / shouldnt be responsive to mouse events. (the canvas parent around it handling mouse enter and leave is)
Are there other possibilities?
1.custom control xaml cutout
2.custom control mouse event
3.ui element adorner class
<Grid x:Name="adholder" Width="{Binding ElementName=thiscontrol, Path=ActualWidth}">
<!-- Add adorning content here -->
<Grid x:Name="gridAnimation" IsHitTestVisible="False"
Height="{Binding ElementName=thiscontrol, Path=ActualHeight}"
Width="{Binding ElementName=thiscontrol, Path=ActualWidth}">
<Grid.CacheMode>
<BitmapCache x:Name="bmpcache" EnableClearType="True"
RenderAtScale="1"
SnapsToDevicePixels="False"
/>
</Grid.CacheMode>
private void thiscontrol_MouseEnter(object sender, MouseEventArgs e)
{
if (StoryboardEnter != null)
{
layer = AdornerLayer.GetAdornerLayer(gridAnimation);
adholder.Children.Clear();
adc = new AdornerContentPresenter(adholder);
adc.Content = gridAnimation;
layer.Add(adc);
StoryboardEnter.RunAnim(gridAnimation, AnimationDirection.To, AnimationReset.Reset);
}
}
//not my code
public class AdornerContentPresenter : Adorner
{
private VisualCollection _Visuals;
private ContentPresenter _ContentPresenter;
public AdornerContentPresenter(UIElement adornedElement)
: base(adornedElement)
{
_Visuals = new VisualCollection(this);
_ContentPresenter = new ContentPresenter();
_Visuals.Add(_ContentPresenter);
}
public AdornerContentPresenter(UIElement adornedElement, Visual content)
: this(adornedElement)
{ Content = content; }
protected override Size MeasureOverride(Size constraint)
{
_ContentPresenter.Measure(constraint);
return _ContentPresenter.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
_ContentPresenter.Arrange(new Rect(0, 0,
finalSize.Width, finalSize.Height));
return _ContentPresenter.RenderSize;
}
protected override Visual GetVisualChild(int index)
{ return _Visuals[index]; }
protected override int VisualChildrenCount
{ get { return _Visuals.Count; } }
public object Content
{
get { return _ContentPresenter.Content; }
set { _ContentPresenter.Content = value; }
}
}
Related
I am trying to implement some fade-in and fade-out animations for a user control in WPF. For the fade-in animation I was able to use the Loaded event to accomplish that.
public sealed partial class NowPlayingView : UserControl
{
public Duration AnimationDuration
{
get { return (Duration)GetValue(AnimationDurationProperty); }
set { SetValue(AnimationDurationProperty, value); }
}
public static readonly DependencyProperty AnimationDurationProperty =
DependencyProperty.Register("AnimationDuration", typeof(Duration), typeof(NowPlayingView), new PropertyMetadata(Duration.Automatic));
public NowPlayingView()
{
Opacity = 0;
InitializeComponent();
Loaded += NowPlayingView_Loaded;
Unloaded += NowPlayingView_Unloaded;
}
private void NowPlayingView_Unloaded(object sender, RoutedEventArgs e)
{
DoubleAnimation animation = new(1.0, 0.0, AnimationDuration);
BeginAnimation(OpacityProperty, animation);
}
private void NowPlayingView_Loaded(object sender, RoutedEventArgs e)
{
DoubleAnimation animation = new (0.0, 1.0, AnimationDuration);
BeginAnimation(OpacityProperty, animation);
}
}
I attempted to use the Unloaded event for the fade-out effect only to find out that the event is fired after the UserControl is removed from the visual tree (when the UserControl is no longer visible or accessible). Is there a way to run some code right before the UserControl "closes", something like the OnClosing event of a Window?
EDIT:
For a bit more context, the UserControl acts as a component of a more complex window. It is activated whenever the Property NowPlayingViewModel is not null and deactivated when null (which I do in order to hide the UserControl). It is when I set the ViewModel to null that I want to run the fade-out animation and I would like to keep the code-behind decoupled from other ViewModel logic.
<!-- Now playing View-->
<ContentControl Grid.RowSpan="3" Grid.ColumnSpan="2" Content="{Binding NowPlayingViewModel}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type viewmodels:NowPlayingViewModel}">
<views:NowPlayingView AnimationDuration="00:00:00.8" />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
From my testing, I couldn't find any good solution to this so far, though I am open to suggestions that lead to similar behavior.
There is no Closing event in UserControl.. but you can get the parent window when UserControl is loaded and implement the fade-out behavior there..
First, Remove Unloaded += NowPlayingView_Unloaded;
Then, modify the Loaded code a bit..
private Window ParentWindow
{
get
{
DependencyObject parentDepObj = this;
do
{
parentDepObj = VisualTreeHelper.GetParent(parentDepObj);
if (parentDepObj is Window parentWindow) return parentWindow;
} while (parentDepObj != null);
return null;
}
}
private void NowPlayingView_Loaded(object sender, RoutedEventArgs e)
{
DoubleAnimation animation = new(0.0, 1.0, AnimationDuration);
BeginAnimation(OpacityProperty, animation);
var parentWindow = this.ParentOfType<Window>();
parentWindow.Closing += WindowClosing;
}
private void WindowClosing(object sender, CancelEventArgs args)
{
var pw = ParentWindow;
pw.Closing -= WindowClosing;
args.Cancel = true;
var anim = new(1.0, 0.0, AnimationDuration);
anim.Completed += (s, _) => pw.Close();
BeginAnimation(OpacityProperty, anim);
}
Optional Note. You could replace the getter of ParentWindow property with a simple call
private Window ParentWindow => this.ParentOfType<Window>();
Where ParentOfType is an extension function in some public static class Utilities..
public static T ParentOfType<T>(this DependencyObject child) where T : DependencyObject
{
var parentDepObj = child;
do
{
parentDepObj = VisualTreeHelper.GetParent(parentDepObj);
if (parentDepObj is T parent) return parent;
} while (parentDepObj != null);
return null;
}
WPF
I have a TreeView (see below) for which I have created an Editing Adorner.
All works correctly. However, I would like to disable the TreeView while editing is in progress. When the TreeView is disabled -- as well as the element being adorned for editing -- the adorner is also being disabled.
How can the TreeView be disabled (IsEnabled = false) but still allow the Adorner to be enabled for editing?
TIA
namespace Doctor_Desk.Views.Adorners
{
public class EditSelectedItemAdorner : Adorner
{
private VisualCollection _Visuals;
private UIElement _UIElement;
private bool _IsCancel = false;
private TextBox _Textbox;
protected override int VisualChildrenCount
{
get
{
return _Visuals.Count;
}
}
// Be sure to call the base class constructor.
public EditSelectedItemAdorner(UIElement adornedElement, IConsultantTreeItem selectedItem)
: base(adornedElement)
{
_UIElement = adornedElement;
adornedElement.Visibility = Visibility.Hidden;
_Textbox = new TextBox
{
Background = Brushes.Pink,
Text = selectedItem.GetText()
};
// The VisualCollection will hold the content of this Adorner.
_Visuals = new VisualCollection(this)
{
// The _Textbox is a logical child of the VisualCollection of the Adorner. The ArrangeOverride and MeasureOverride
// will set up the Grid control for correct rendering.
_Textbox // Adding a single control for display.
};
}
/// The adorner placement is always relative to the top left corner of the adorned element. The children of the adorner are located by
/// implementing ArrangeOverride. The only other way to position the Adorner content is to use the AdornerPanel Class
/// with it's AdornerPlacementCollection Methods.
///
/// Overriding the default ArrangeOverride and MeasureOverride allows a control to be placed and diplayed in the VisualCollection.
protected override Size ArrangeOverride(Size finalSize)
{
//TextBox child = _Visuals[0] as TextBox;
//FrameworkElement element = _UIElement as FrameworkElement;
//if (element != null)
//{
// Size textBoxSize = new Size(Math.Max(150, element.DesiredSize.Width), Math.Max(30, element.DesiredSize.Height));
// Point location = new Point((this.Width - textBoxSize.Width) / 2,
// (textBoxSize.Height - this.Height) / 2);
// child.Arrange(new Rect(location, textBoxSize));
//}
TextBox child = _Visuals[0] as TextBox;
Rect adornedElementRect = new Rect(this.AdornedElement.DesiredSize);
child.Arrange(adornedElementRect);
return base.ArrangeOverride(finalSize);
}
// Overriding the default ArrangeOverride and MeasureOverride allows a control to be diplayed in the VisualCollection.
protected override Size MeasureOverride(Size constraint)
{
_Textbox.Measure(constraint);
return _Textbox.DesiredSize;
}
protected override Visual GetVisualChild(int index)
{
return _Visuals[index];
}
// A common way to implement an adorner's rendering behavior is to override the OnRender
// method, which is called by the layout system as part of a rendering pass.
protected override void OnRender(DrawingContext drawingContext)
{
Rect adornedElementRect = new Rect(this.AdornedElement.DesiredSize);
// Some arbitrary drawing implements.
SolidColorBrush renderBrush = new SolidColorBrush(Colors.Green)
{
Opacity = 0.2
};
Pen renderPen = new Pen(new SolidColorBrush(Colors.Navy), 1.5);
double renderRadius = 5.0;
// Draw a circle at each corner.
drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.TopLeft, renderRadius, renderRadius);
drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.TopRight, renderRadius, renderRadius);
drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.BottomLeft, renderRadius, renderRadius);
drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.BottomRight, renderRadius, renderRadius);
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
// Using a DependencyProperty as the backing store for Text. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(EditSelectedItemAdorner), new PropertyMetadata(string.Empty));
}
}
<TreeView Grid.Row="2" Grid.Column="2" Grid.RowSpan="4" Width="400"
ItemsSource="{Binding SpecialityTree}"
>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type tree:SpecialityTreeItem}" ItemsSource="{Binding Offices}" >
<TextBlock Text="{Binding Title}" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type tree:OfficeTreeItem}" ItemsSource="{Binding Doctors}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type tree:DoctorTreeItem}">
<StackPanel Orientation="Horizontal" >
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding LastName}" />
</StackPanel>
</DataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True"/>
</Style>
</TreeView.ItemContainerStyle>
<i:Interaction.Behaviors>
<b:ConsultantTreeViewBehavior
EditSelectedItem="{Binding IsEditing}"
SelectedItem="{Binding SelectedTreeItem, Mode=TwoWay}"
Text="{Binding EditingResult, Mode=OneWayToSource}"
/>
</i:Interaction.Behaviors>
namespace Doctor_Desk.Views.Behaviors
{
public class ConsultantTreeViewBehavior : Behavior<TreeView>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
if (AssociatedObject != null)
{
AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
}
}
private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
SelectedItem = e.NewValue;
}
#region SelectedItem Property
public object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(object), typeof(ConsultantTreeViewBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged));
private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is TreeViewItem item)
{
item.SetValue(TreeViewItem.IsSelectedProperty, true);
}
}
#endregion
#region [EditSelectedItem]
public bool EditSelectedItem
{
get { return (bool)GetValue(EditSelectedItemProperty); }
set { SetValue(EditSelectedItemProperty, value); }
}
private EditSelectedItemAdorner _Adorner;
public AdornerLayer myAdornerLayer { get; set; }
// Using a DependencyProperty as the backing store for EditSelectedItem. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EditSelectedItemProperty =
DependencyProperty.Register("EditSelectedItem", typeof(bool), typeof(ConsultantTreeViewBehavior), new PropertyMetadata(false, OnEditSelectedItem));
private static void OnEditSelectedItem(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
var d = sender as ConsultantTreeViewBehavior;
var h = d.AssociatedObject as TreeView;
TreeViewItem tvi = h
.ItemContainerGenerator
.ContainerFromItemRecursive(h.SelectedItem);
if (tvi is UIElement myItem)
{
d._Adorner = new EditSelectedItemAdorner(myItem, (IConsultantTreeItem)h.SelectedItem);
// Must have BindingMode.TwoWay for the Adorner to update this ConsultantTreeViewBehavior.
var bindingText = new Binding
{
Source = d,
Path = new PropertyPath(TextProperty),
Mode = BindingMode.TwoWay
};
// The TextProperty binds the Text from the EditSelectedItemAdorner to the ConsultantManagerViewModel.
BindingOperations.SetBinding(d._Adorner, EditSelectedItemAdorner.TextProperty, bindingText);
d.myAdornerLayer = AdornerLayer.GetAdornerLayer(myItem);
d.myAdornerLayer.Add(d._Adorner);
}
}
}
#endregion
#region[Text - xaml binding]
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
// Using a DependencyProperty as the backing store for Text. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(ConsultantTreeViewBehavior), new PropertyMetadata(string.Empty,
(s, e) =>
{
}));
#endregion
}
}
Well, in leu of any other answer, it would appear that disabling selection in the TreeView disables all the elements of the TreeView -- including the selected branch. When the selected branch is disabled, so is its editing adorner. Therefore, my workaround is simply to cover that part of the TreeView which is being displayed.
In particular, in the OnRender override, add:
// Find offset of selected item from top of the tree.
GeneralTransform gt = AdornedElement.TransformToVisual(_TreeView);
Point offset_to_tree_top = gt.Inverse.Transform(new Point(0, 0));
drawingContext.DrawRectangle(Brushes.DimGray, null, new Rect(
offset_to_tree_top, _TreeView.DesiredSize));
This puts a DimGray color over the whole tree except for the editing adorner. Thus, user selection of any other tree item can not occur.
I have a ListBox of Items and a Search TextBox and Search Button, i want to enter the search text in the TextBox and Click Search Button so the ListBox highlight that item and get it on screen (for lengthy list).
Is it possible to do this using ICollectionView? and if not possible how to implement this scenario.
Note: after googling i found all samples talking about Filtering but i need searching.
Thanks for bearing with us.
You can achieve this by implementing a Prism Behavior:
public class AutoScrollingBehavior:Behavior<ListBox>
{
protected override void OnAttached()
{
base.OnAttached();
var itemsSource = AssociatedObject.ItemsSource as ICollectionView;
if (itemsSource == null)
return;
itemsSource.CurrentChanged += ItemsSourceCurrentChanged;
}
void ItemsSourceCurrentChanged(object sender, EventArgs e)
{
AssociatedObject.ScrollIntoView(((ICollectionView)sender).CurrentItem);
AssociatedObject.Focus();
}
}
Another approach is listening to ListBox.SelectionChanged instead of ICollectionView.CurrentChanged.
public class AutoScrollingBehavior:Behavior<ListBox>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += AssociatedObjectSelectionChanged;
}
void AssociatedObjectSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count <= 0)
return;
AssociatedObject.ScrollIntoView(e.AddedItems[0]);
AssociatedObject.Focus();
}
}
On Xaml:
<ScrollViewer Height="200">
<ListBox x:Name="listbox" ItemsSource="{Binding Path=NamesView}" SelectionMode="Single"
IsSynchronizedWithCurrentItem="True">
<i:Interaction.Behaviors>
<local:AutoScrollingBehavior/>
</i:Interaction.Behaviors>
</ListBox>
</ScrollViewer>
Inside searching command, you set NamesView.MoveCurrentTo(foundItem). However this approach will only scroll to the edge, instead of center, might you expected. If you want it to scroll to the center, you might need ItemContainerGenerator.
In your view model who holds the ICollectionView:
private string _searchText;
public string SearchText
{
get { return _searchText; }
set
{
_searchText = value;
RaisePropertyChanged("SearchText");
}
}
private ICommand _searchCommand;
public ICommand SearchCommand
{
get { return _searchCommand ?? (_searchCommand = new DelegateCommand(Search)); }
}
private void Search()
{
var item = _names.FirstOrDefault(name => name == SearchText);
if (item == null) return;
NamesView.MoveCurrentTo(item);
}
On Xaml, bind TextBox.Text to SearchText and bind search button's Command to SearchCommand.
Hope it can help.
I have a flyout panel (as illustrated below), which if undocked should be invisible when the mouse leaves the panel area in general.
However, I don't want the panel to close if any of these conditions occur:
1) The user opens a ContextMenu
2) The user chooses a ComboBox item that falls below the panel (as illustrated above)
3) A confirmation dialog that comes up due to a user action (such as deleting an item in the DataGrid)
It's easy to track context menu operations (ContextMenuOpening and ContextMenuClosing events) to handle the first case, but I haven't found any good ways yet to handle the other two cases, in particular tracking dialogs being opened.
Any ideas?
My flyout panel is just a grid whose visibility and content is determined in code behind:
<Grid Name="UndockedGrid" ContextMenuOpening="Grid_ContextMenuOpening" ContextMenuClosing="Grid_ContextMenuClosing" MouseLeave="Grid_MouseLeave">
<!-- Toolbox (undocked) -->
<ScrollViewer Name="ToolBoxUndockedViewer">
<StackPanel Name="ToolBoxUndockedPanel" />
</ScrollViewer>
</Grid>
I took a RoutedEvent approach to this problem, since my view models do not handle dialog workflows directly in this version.
To get the behavior I wanted for the data grid combo boxes, I extended DataGrid to add the routed events I wanted to track:
public class RoutableDataGrid : DataGrid
{
public static readonly RoutedEvent ElementOpenedEvent = EventManager.RegisterRoutedEvent("ElementOpened", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(RoutableDataGrid));
public event RoutedEventHandler ElementOpened
{
add { AddHandler(ElementOpenedEvent, value); }
remove { RemoveHandler(ElementOpenedEvent, value); }
}
public static readonly RoutedEvent ElementClosedEvent = EventManager.RegisterRoutedEvent("ElementClosed", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(RoutableDataGrid));
public event RoutedEventHandler ElementClosed
{
add { AddHandler(ElementClosedEvent, value); }
remove { RemoveHandler(ElementClosedEvent, value); }
}
public void RaiseElementOpened()
{
RaiseEvent(new RoutedEventArgs(RoutableDataGrid.ElementOpenedEvent, this));
}
public void RaiseElementClosed()
{
RaiseEvent(new RoutedEventArgs(RoutableDataGrid.ElementClosedEvent, this));
}
}
And the DataGridComboBoxColumn was extended to fire the new routed events:
public class BindableDataGridComboBoxColumn : DataGridComboBoxColumn
{
protected RoutableDataGrid ParentGrid { get; set; }
protected FrameworkElement Element { get; set; }
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
Element = base.GenerateEditingElement(cell, dataItem);
Element.MouseEnter += new System.Windows.Input.MouseEventHandler(element_MouseEnter);
Element.MouseLeave += new System.Windows.Input.MouseEventHandler(element_MouseLeave);
CopyItemsSource(Element);
return Element;
}
void element_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
{
if (ParentGrid != null)
{
ParentGrid.RaiseElementClosed();
}
}
void element_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
if (ParentGrid != null)
{
ParentGrid.RaiseElementOpened();
}
}
}
More of a hack, since MessageBox is a sealed class, I had to build a wrapper class to fire routed events using a MessageBox (and put an instance of this element in the xaml so that it's in the visual tree):
public class RoutedEventPlaceHolder : UIElement
{
public static readonly RoutedEvent ElementOpenedEvent = EventManager.RegisterRoutedEvent("ElementOpened", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(RoutedEventPlaceHolder));
public event RoutedEventHandler ElementOpened
{
add { AddHandler(ElementOpenedEvent, value); }
remove { RemoveHandler(ElementOpenedEvent, value); }
}
public static readonly RoutedEvent ElementClosedEvent = EventManager.RegisterRoutedEvent("ElementClosed", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(RoutedEventPlaceHolder));
public event RoutedEventHandler ElementClosed
{
add { AddHandler(ElementClosedEvent, value); }
remove { RemoveHandler(ElementClosedEvent, value); }
}
public void RaiseElementOpened()
{
RaiseEvent(new RoutedEventArgs(RoutedEventPlaceHolder.ElementOpenedEvent, this));
}
public void RaiseElementClosed()
{
RaiseEvent(new RoutedEventArgs(RoutedEventPlaceHolder.ElementClosedEvent, this));
}
public MessageBoxResult ShowMessageBox(string text, string caption, MessageBoxButton button)
{
RaiseElementOpened();
MessageBoxResult result = MessageBox.Show(text, caption, button);
RaiseElementClosed();
return result;
}
}
Then finally I can subscribe to the new routed events in the flyout panel:
<Grid Name="UndockedGrid" lib:RoutedEventPlaceHolder.ElementOpened="Grid_ElementOpened" lib:RoutableDataGrid.ElementOpened="Grid_ElementOpened" ContextMenuOpening="Grid_ContextMenuOpening" ContextMenuClosing="Grid_ContextMenuClosing" MouseLeave="Grid_MouseLeave">
<!-- Toolbox (undocked) -->
<ScrollViewer Grid.Row="0" Grid.Column="0" Name="ToolBoxUndockedViewer" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Visibility="Collapsed" MouseLeave="ToolBoxUndockedViewer_MouseLeave">
<StackPanel Name="ToolBoxUndockedPanel" MinWidth="200" Background="{StaticResource ControlBackgroundBrush}" />
</ScrollViewer>
</Grid>
This is not an ideal solution. I'll accept any other answers that are more elegant.
At a glance:
My app displays an ItemsControl containing a Canvas as its ItemsPanel. The ItemsControl is bound to a collection of objects, each having Left/Top/Width/Height properties. A DataTemplate is used to generate rectangles that are rendered in the Canvas and positioned correctly (binding on the Left and Top properties).
How can I implement drag/drop to move these rectangles around the Canvas?
Background for my question:
My WP7 app displays a "CanvasItemsControl" defined as follows:
public class CanvasItemsControl : ItemsControl
{
public string XBindingPath { get; set; }
public string YBindingPath { get; set; }
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
FrameworkElement contentitem = element as FrameworkElement;
if (XBindingPath != null && YBindingPath != null)
{
Binding xBinding = new Binding(XBindingPath);
Binding yBinding = new Binding(YBindingPath);
if (contentitem != null)
{
contentitem.SetBinding(Canvas.LeftProperty, xBinding);
contentitem.SetBinding(Canvas.TopProperty, yBinding);
}
}
base.PrepareContainerForItemOverride(element, item);
}
}
and used in XAML as follows:
<hh:CanvasItemsControl Grid.Row="1" x:Name="TheItemsControl"
Style="{StaticResource CanvasItemsControlStyle}"
ItemsSource="{Binding AllObjects}"
XBindingPath="Left" YBindingPath="Top" />
This is the style for the CanvasItemsControl:
<Style x:Key="CanvasItemsControlStyle" TargetType="local:CanvasItemsControl">
<Setter Property="ItemTemplate" Value="{StaticResource ObjectTemplate}"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
And this is the DataTemplate I use to render my class:
<DataTemplate x:Key="ObjectTemplate" >
<Border Background="{Binding Brush}"
Width="{Binding Width}"
Height="{Binding Height}">
<TextBlock Text="{Binding Description}"/>
</Border>
</DataTemplate>
The source of the CanvasItemsControl is a collection of objects that have the properties Left, Top, Width, Height, Brush, etc.
My question
As you can see, the end result is, as you add items to the AllObjects collection, each object gets rendered and positioned correctly in the canvas. Now I need to drag/drop/move these objects around the canvas. What approach would you advise me to use to implement drag/drop? Can you please guide me through the process?
Thank you
Here's the solution to my question (at least the best one in my opinion):
1) Use of a regular Canvas as opposed of a custom control inherited from Canvas.
2) Use of a user control taking the data context (the instance of my business entity) via constructor
3) The binding between the Left/Top properties of my business class and the Canvas.Left/Top is declared at the UserControl level.
4) Use of a custom behavior inheriting from System.Windows.Interactivity.Behavior. This behavior is attached to the User Control.
I would like to acknowlege Calvin Schrotenboer and Joe Gershgorin for their immense help.
<!--____ The UserControl ____-->
<UserControl... Canvas.Left={Binding Left}" Canvas.Top={Binding Top}">
<Grid.... layout of the UserControl instead of using a DataTemplate/>
<i:Interaction.Behaviors>
<MyExample:MyMouseDragElementBehavior/>
</i:Interaction.Behaviors>
</UserControl>
The custom behavior:
public class MyMouseDragElementBehavior : Behavior<FrameworkElement>
{
public event MouseEventHandler DragBegun;
public event MouseEventHandler DragFinished;
public event MouseEventHandler Dragging;
private Point relativePosition;
public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.Register("IsEnabled", typeof(bool), typeof(MyMouseDragElementBehavior), new PropertyMetadata(true));
public bool IsEnabled
{
get
{
return (bool)GetValue(IsEnabledProperty);
}
set
{
SetValue(IsEnabledProperty, value);
}
}
protected override void OnAttached()
{
AssociatedObject.AddHandler(
UIElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler(OnMouseLeftButtonDown), false);
base.OnAttached();
}
protected override void OnDetaching()
{
AssociatedObject.RemoveHandler(
UIElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler(OnMouseLeftButtonDown));
base.OnDetaching();
}
private static int zIndex = 0;
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (!IsEnabled)
{
return;
}
zIndex++;
Canvas.SetZIndex(AssociatedObject, zIndex);
StartDrag(e.GetPosition(AssociatedObject));
if (DragBegun != null)
{
DragBegun(this, e);
}
}
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
AssociatedObject.ReleaseMouseCapture();
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
HandleDrag(e.GetPosition(AssociatedObject));
if (Dragging != null)
{
Dragging(this, e);
}
}
internal void HandleDrag(Point newPositionInElementCoordinates)
{
double x = newPositionInElementCoordinates.X - relativePosition.X;
double y = newPositionInElementCoordinates.Y - relativePosition.Y;
if (AssociatedObject != null)
{
var currentLeft = Canvas.GetLeft(AssociatedObject);
var currentTop = Canvas.GetTop(AssociatedObject);
Canvas.SetLeft(AssociatedObject, currentLeft + x);
Canvas.SetTop(AssociatedObject, currentTop + y);
}
}
internal void StartDrag(Point positionInElementCoordinates)
{
relativePosition = positionInElementCoordinates;
AssociatedObject.CaptureMouse();
AssociatedObject.MouseMove += OnMouseMove;
AssociatedObject.LostMouseCapture += OnLostMouseCapture;
AssociatedObject.AddHandler(UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler(OnMouseLeftButtonUp), false);
}
internal void EndDrag()
{
AssociatedObject.MouseMove -= OnMouseMove;
AssociatedObject.LostMouseCapture -= OnLostMouseCapture;
AssociatedObject.RemoveHandler(
UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler(OnMouseLeftButtonUp));
}
private void OnLostMouseCapture(object sender, MouseEventArgs e)
{
EndDrag();
if (DragFinished != null)
{
DragFinished(this, e);
}
}
}