I've got a Collection of items bound to an ItemsControl element.
I need to enable the mouse dragging behavior for each element but I can't seem to make it work.
Adding this code
<i:Interaction.Behaviors>
<el:MouseDragElementBehavior/>
</i:Interaction.Behaviors>
will make it work but only if I put my items outside the ItemsControl.
Here's the code of the ItemsControl component:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<utils:TemplateSelector Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Am I doing something wrong?
EDIT: New code but still not working (with and without the Canvas)
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<utils:TemplateSelector Content="{Binding}">
<i:Interaction.Behaviors>
<el:MouseDragElementBehavior/>
</i:Interaction.Behaviors>
</utils:TemplateSelector>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Seems like that once you start the bounty you suddenly find a solution which you were not able to find in the past months.
I'm answering to add my solution, that currently works but surely can be done better.
What I'm proposing is based on two different sources:
A gesture-driven Windows Phone to-do application by Colin Eberhardt
How to find element in visual tree? wp7 answered by E.Z. Hart
Let's start with the XAML:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="{StaticResource PhoneForegroundBrush}">
<utils:TemplateSelector Content="{Binding}"/>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener
DragStarted="GestureListener_OnDragStarted"
DragDelta="GestureListener_OnDragDelta"
DragCompleted="GestureListener_OnDragCompleted"/>
</toolkit:GestureService.GestureListener>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas x:Name="WidgetsCanvas" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
(toolkit comes from Windows Phone Toolkit)
Now let's see the implementation of the three drag handlers.
private int _zindex; //Used to keep the moved element on the top, not working for now
private FrameworkElement _movedObject; //The element that we're moving. Used to avoid moving multiple items if they overlap
private void GestureListener_OnDragStarted(object sender, DragStartedGestureEventArgs e)
{
if (_movedObject != null) return; // We're already moving something!
// Initialize the drag
var fe = sender as FrameworkElement; // The element that we want to move
(fe as Border).BorderThickness = new Thickness(5); // A simple effect to mark the element on the screen
_movedObject = fe; // We set the current object to the one which is moving
Canvas.SetZIndex(fe, _zindex++); // This should take the moved object on the top but it's not working
}
private void GestureListener_OnDragDelta(object sender, DragDeltaGestureEventArgs e)
{
var fe = sender as FrameworkElement;
if (!fe.Equals(_movedObject)) return; // We change the object's position only if this is the one who started the event
var offset = DragManager.GetOffset(fe); // We get the current position
var canvas = DragManager.FindChild<Canvas>(Application.Current.RootVisual, "ItemsCanvas"); // We need the container of our object to force it to stay inside the container
//The new position is given by the old one plus the change reported by the event
var horizontalOffset = offset.HorizontalValue + e.HorizontalChange;
var verticalOffset = offset.VerticalValue + e.VerticalChange;
// We need to check if the new position is outside our container's bounds
if (horizontalOffset < 0) horizontalOffset = 0;
else if (horizontalOffset > (canvas.ActualWidth - fe.ActualWidth)) horizontalOffset = canvas.ActualWidth - fe.ActualWidth;
if (verticalOffset < 0) verticalOffset = 0;
else if (verticalOffset > (canvas.ActualHeight - fe.ActualHeight)) verticalOffset = canvas.ActualHeight - fe.ActualHeight;
// Once we've got everything set, we can move our component
DragManager.SetOffset(fe, horizontalOffset, verticalOffset);
}
private void GestureListener_OnDragCompleted(object sender, DragCompletedGestureEventArgs e)
{
var fe = sender as FrameworkElement;
(fe as Border).BorderThickness = new Thickness(0); // We undo our effect
_movedObject = null; // The movement is done so we can reset our current object and wait for a new one to come
}
the handlers work with a class that I called DragManager. Its code it's quite simplet:
public static class DragManager
{
public static void SetOffset(FrameworkElement fe, double horizontalOffset, double verticalOffset)
{
var trans = new TranslateTransform
{
X = horizontalOffset,
Y = verticalOffset
};
// I don't know what may change, in terms of performance, between applying the transform or just changing the margins. I'm using the margins because the transform may be needed for some other purpose
//fe.RenderTransform = trans;
fe.Margin = new Thickness(horizontalOffset, verticalOffset, 0, 0); // We just change our object's margins to reflect its new position
// We store the current position in the objects Tag (maybe there's a better solution but I'm quite new to C#/xaml)
fe.Tag = new Offset
{
VerticalValue = verticalOffset,
HorizontalValue = horizontalOffset,
Transform = trans
};
}
public static Offset GetOffset(FrameworkElement fe)
{
if (fe.Tag == null) fe.Tag = new Offset();
return (Offset)fe.Tag;
}
public struct Offset
{
public double HorizontalValue { get; set; }
public double VerticalValue { get; set; }
public TranslateTransform Transform { get; set; }
}
public static T FindChild<T>(DependencyObject parent, string childName) where T : DependencyObject
{
// Confirm parent and childName are valid.
if (parent == null)
{
return null;
}
T foundChild = null;
var childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (var i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
// If the child is not of the request child type child
var childType = child as T;
if (childType == null)
{
// recursively drill down the tree
foundChild = FindChild<T>(child, childName);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null)
{
break;
}
}
else if (!String.IsNullOrEmpty(childName))
{
var frameworkElement = child as FrameworkElement;
// If the child's name is set for search
if (frameworkElement != null && frameworkElement.Name == childName)
{
// if the child's name is of the request name
foundChild = (T)child;
break;
}
// Need this in case the element we want is nested
// in another element of the same type
foundChild = FindChild<T>(child, childName);
}
else
{
// child element found.
foundChild = (T)child;
break;
}
}
return foundChild;
}
}
As I already said, this code is working right now.
If anybody has some suggestion to improve it, please write it here so that I can see/test a better way to do this kind of stuff!
Related
I need to access the scrollviewer of a listview from the codebehind.
here is the definition of my listview
<ListView Grid.Row="1" ItemsSource="{Binding Path=SpecList, UpdateSourceTrigger=PropertyChanged}"
Name="mylistview"
ItemTemplate="{StaticResource SpecElementTemplate}"
Background="{StaticResource EnvLayout}"
ScrollViewer.HorizontalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ItemContainerStyle="{StaticResource MyStyle}"
BorderBrush="Blue"
BorderThickness="20"
Margin="-2">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
How can I get the scrollviewer?
Thank you
Andrea
There are several ways to get the ScrollViewer. Simplest solution is to get the the first child of the first child of the ListView. This means get the Border and the ScrollViewer inside this Border like described in
this answer:
// Get the border of the listview (first child of a listview)
Decorator border = VisualTreeHelper.GetChild(mylistview, 0) as Decorator;
// Get scrollviewer
ScrollViewer scrollViewer = border.Child as ScrollViewer;
A second way is to scan all childrens recursive to find the ScrollViewer. This is described in the answer by Matt Hamilton in this question. You can simply use this function to get the ScrollViewer.
ScrollViewer scrollViewer = GetChildOfType<ScrollViewer>(mylistview);
This second solution is much more generic and will also work if the template of your ListView was edited.
Use VisualTreeHelper class to access any child control.
Psudeo code to your case:
//Declare a scroll viewer object.
ScrollViewer sViewer = default(ScrollViewer );
//Start looping the child controls of your listview.
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(YOUR_LISTVIEW_OBJECT.VisualParent ); i++)
{
// Retrieve child visual at specified index value.
Visual childVisual = (Visual)VisualTreeHelper.GetChild(YOUR_LISTVIEW_OBJECT.VisualParent , i);
ScrollViewer sViewer = childVisual as ScrollViewer;
//You got your scroll viewer. Stop looping.
if (sViewer != null)
{
break;
}
}
I also suggest using the CollectionChanged event. In this code, the CollectionChanged event handler is added to the codebehind after the view model has been loaded. Then, each time the collection changes we scroll to the bottom of the listview. Here is an important point. The scrollviewer child of the list view might not yet be completely rendered when our events start firing. Hence we will get exceptions if we try to use the VisualTreeHelper.GetChild method. So, we have to first attempt to get the scrollviewer and then ignore its positioning if it is not yet available.
private void ReceivedItems_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// Make sure the items source property in the viewmodel has some items
if (myViewModel.ReceivedItems.Count > 0)
{
var aScrollViewer = RcvdListView.GetChildOfType<ScrollViewer>();
// Make sure the scrollviewer exists before trying to position it
if (aScrollViewer != null)
{
aScrollViewer.ScrollToBottom();
}
}
}
Listview's ScrollViewer should be accessible after LayoutUpdated. You could hook on LayoutUpdated and then get if from Visual tree
private static void ListView_LayoutUpdated(object sender, EventArgs e)
{
var listview = (ListView)sender;
var viewer = listview.GetFirstChildOfType<ScrollViewer>();
}
public static T GetFirstChildOfType<T>(this DependencyObject dependencyObject) where T : DependencyObject
{
if (dependencyObject == null)
{
return null;
}
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
{
var child = VisualTreeHelper.GetChild(dependencyObject, i);
var result = (child as T) ?? GetFirstChildOfType<T>(child);
if (result != null)
{
return result;
}
}
return null;
}
Is there any way to create a copy of canvas with the new canvas will be as a box of children elements of the first canvas ?
first canvas
second canvas
i want something like that i am showing in the second canvas.
You can do this by deriving from Canvas and overriding OnVisualChildrenChanged.
You must make changes public so that your copied Canvas (let's call it the target Canvas) can be properly updated.
What you want to do is fairly complex if you need to listen to changes for when the source canvas has children whose properties are updated. In this case, you must again notify your target canvas of changes, which can be done by adding bindings.
XAML
<Window x:Class="StackOverflow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StackOverflow"
Title="MainWindow" Height="437.042" Width="525">
<Grid>
<Border Height="213" Margin="10,10,10,0" VerticalAlignment="Top" BorderBrush="Black" BorderThickness="1">
<local:ObservableCanvas x:Name="CanvasSource"
VisualChildrenChanged="CanvasSource_VisualChildrenChanged">
<!-- Add some elements -->
<Ellipse Width="10" Height="10" Fill="Red"/>
<Ellipse Canvas.Left="10" Canvas.Top="10"
Width="20" Height="20" Fill="Green"/>
<Ellipse Canvas.Left="30" Canvas.Top="30"
Width="30" Height="30" Fill="Blue"/>
<Ellipse Canvas.Left="60" Canvas.Top="60"
Width="40" Height="40" Fill="Yellow"/>
</local:ObservableCanvas>
</Border>
<Border Margin="148,228,148,10" BorderBrush="Black" BorderThickness="1">
<Canvas x:Name="CanvasTarget" Loaded="CanvasTarget_Loaded"/>
</Border>
</Grid>
</Window>
At design time, this is the window:
In particular, notice the VisualChildrenChanged="CanvasSource_VisualChildrenChanged". This is how the changes are made public. We handle these changes in the code-behind for the MainWindow.
ObservableCanvas
//This class notifies listeners when child elements are added/removed & changed.
public class ObservableCanvas : Canvas, INotifyPropertyChanged
{
//Implement INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
//This function should be called when a child element has a property updated.
protected virtual void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
//Create a routed event to indicate when the ObservableCanvas has changes to its child collection.
public static readonly RoutedEvent VisualChildrenChangedEvent = EventManager.RegisterRoutedEvent(
"VisualChildrenChanged",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(ObservableCanvas));
//Create CLR event handler.
public event RoutedEventHandler VisualChildrenChanged
{
add { AddHandler(VisualChildrenChangedEvent, value); }
remove { RemoveHandler(VisualChildrenChangedEvent, value); }
}
//This function should be called to notify listeners
//to changes to the child collection.
protected virtual void RaiseVisualChildrenChanged()
{
RaiseEvent(new RoutedEventArgs(VisualChildrenChangedEvent));
}
//Override to make the changes public.
protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
//Create bindings here to properties you need to monitor for changes.
//This example shows how to listen for changes to the Fill property.
//You may need to add more bindings depending on your needs.
Binding binding = new Binding("Fill");
binding.Source = visualAdded;
binding.NotifyOnTargetUpdated = true;
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
SetBinding(Shape.FillProperty, binding);
//Instruct binding target updates to cause a global property
//update for the ObservableCanvas.
//This will make child property changes visible to the outside world.
Binding.AddTargetUpdatedHandler(this,
new EventHandler<DataTransferEventArgs>((object sender, DataTransferEventArgs e) =>
{
RaisePropertyChanged("Fill");
}));
//Notify listeners that the ObservableCanvas had an item added/removed.
RaiseVisualChildrenChanged();
}
}
As seen above, this class primarily handles the adding/removing of child elements to/from the ObservableCanvas. You may need to keep a global list of bindings for the elements added, and you may need to consider destroying bindings when elements are removed.
Code-Behind
Lastly, we need to handle the publicly made notifications so that the target Canvas can be updated. For simplicity, we do this in the MainWindow's code-behind.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//Demonstrate that children added from inside the code-behind are reflected.
Rectangle newRectangle = new Rectangle() { Width = 50, Height = 50, Fill = Brushes.Orange };
CanvasSource.Children.Add(newRectangle);
Canvas.SetLeft(newRectangle, 100);
Canvas.SetTop(newRectangle, 100);
CanvasSource.PropertyChanged += CanvasSource_PropertyChanged;
//Also, demonstrate that child property changes can be seen.
DispatcherTimer timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(2) };
timer.Tick += (sender, args) =>
{
timer.Stop();
newRectangle.Fill = Brushes.Brown;
};
timer.Start();
}
//This event handler is called when a child is added to or removed from
//the ObservableCanvas.
private void CanvasSource_VisualChildrenChanged(object sender, RoutedEventArgs e)
{
ObservableCanvas source = sender as ObservableCanvas;
if (source == null) return;
CopyElements(source);
}
//This event handler is called when a child element of the ObservableCanvas
//has a property that changes.
void CanvasSource_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
ObservableCanvas source = sender as ObservableCanvas;
if (source == null) return;
CopyElements(source);
}
//This event handler is used to ensure that the target Canvas
//has the elements copied from the source when initialized.
private void CanvasTarget_Loaded(object sender, RoutedEventArgs e)
{
CopyElements(CanvasSource);
}
//This function creates a brand new copy of the ObservableCanvas's
//children and puts it into the target Canvas.
private void CopyElements(ObservableCanvas source)
{
if (CanvasTarget == null) return;
CanvasTarget.Children.Clear(); //Start from scratch.
foreach (UIElement child in source.Children)
{
//We need to create a deep clone of the elements to they copy.
//This is necessary since we can't add the same child to two different
//UIlements.
UIElement clone = (UIElement)XamlReader.Parse(XamlWriter.Save(child));
CanvasTarget.Children.Add(clone);
}
}
}
Final Results
Here is the window upon startup. It has a copy of the elements in the target Canvas.
And since we have a property change occur 2 seconds after initialization (see the code-behind), here is the window after the change occurs:
I have an answer with lines as children, but i want a better solution.
Point firstPoint = new Point(DrawCanvas.ActualWidth, DrawCanvas.ActualHeight);
Point endPoint = new Point(0, 0);
foreach (Line element in DrawCanvas.Children)
{
double x = element.X1;
double y = element.Y1;
if (x < firstPoint.X)
firstPoint.X = x;
if (y < firstPoint.Y)
firstPoint.Y = y;
if (element.X2 > endPoint.X)
endPoint.X = element.X2;
if (element.Y2 > endPoint.Y)
endPoint.Y = element.Y2;
}
double offsetX = firstPoint.X - 5;
double offsetY = firstPoint.Y - 5;
var children = DrawCanvas.Children.Cast<Line>().ToArray();
DrawCanvas.Children.Clear();
Rectangle rect = new Rectangle();
rect.Stroke = new SolidColorBrush(Color.FromRgb(0, 111, 0));
rect.Fill = new SolidColorBrush(Color.FromRgb(0, 111, 111));
rect.Width = endPoint.X - offsetX + 5;
rect.Height = endPoint.Y - offsetY + 5;
Canvas.SetLeft(rect, 0);
Canvas.SetTop(rect, 0);
newCanvas.Children.Add(rect);
newCanvas.Width = rect.Width;
newCanvas.Height = rect.Height;
foreach (Line element in children)
{
Line newLine = element;
newLine.X1 = element.X1 - offsetX;
newLine.X2 = element.X2 - offsetX;
newLine.Y1 = element.Y1 - offsetY;
newLine.Y2 = element.Y2 - offsetY;
newCanvas.Children.Add(newLine);
}
I want a listbox that will show all the images and text "layers" that I have on my Canvas in silverlight. The code I have currently crashes when I try to view the listbox or when I'm viewing the listbox when I add an element. I can't figure out why. Can someone point me in the right direction with this?
XML -
<Grid DataContext="{Binding Path=Project}">
...
...
<TextBlock Name="textBlock1" Text="Layers" Margin="18,16,0,0" />
<StackPanel Grid.Row="1" Grid.RowSpan="2" Grid.ColumnSpan="2">
<ListBox ItemsSource="{Binding Path=Elements}" Height="175" Name="listBox1" Width="172"/>
</StackPanel>
</Grid>
Project.cs
//List of elements
private ObservableCollection<FrameworkElement> elements;
public ObservableCollection<FrameworkElement> Elements
{
get { return elements; }
set
{
elements = value;
NotifyPropertyChanged("Elements");
}
}
// An example of how an element is added to the Elements collection
// There are also image elements added similarly
private void AddTextElement(object param)
{
TextBlock textBlock = new TextBlock();
textBlock.Text = "New Text";
textBlock.Foreground = new SolidColorBrush(Colors.Gray);
textBlock.FontSize = 25;
textBlock.FontFamily = new FontFamily("Arial");
textBlock.Cursor = Cursors.Hand;
textBlock.Tag = null;
this.Elements.Add(textBlock);
numberOfElements++;
this.SelectedElement = textBlock;
this.selectedTextElement = textBlock;
}
private void AddImageElement(object param)
{
bool? gotImage;
string fileName;
BitmapImage imageSource = GetImageFromLocalMachine(out gotImage, out fileName);
if (gotImage == true)
{
Image image = new Image();
OrderElements(image);
image.Name = fileName;
image.Source = imageSource;
image.Height = imageSource.PixelHeight;
image.Width = imageSource.PixelWidth;
image.MaxHeight = imageSource.PixelHeight;
image.MaxWidth = imageSource.PixelWidth;
image.Cursor = Cursors.Hand;
image.Tag = null;
AddDraggingBehavior(image);
image.MouseLeftButtonUp += element_MouseLeftButtonUp;
this.Elements.Add(image);
numberOfElements++;
this.SelectedElement = image;
this.SelectedImageElement = image;
}
}
One reason might be, because you bind using Path property in your Grid element.
You should use binding source, and set your Project object as a staticresource which you can point to when you call binding source.
Like this:
<Window
xlmns:local="NamespaceOfMyProject">
<Window.Resources>
<local:Project x:key="MyProjectResource" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource MyProjectResource}}>
....
</Grid>
....
</Window>
Reason is: You use "Source" when you point to objects, and "Path" when you point to properties.
Another way to set the DataContext is to do it in the codebehind, using this C# code. But first give your grid a name, so it can be referenced in the codebehind:
<Grid x:Name="myGrid">
Codebehind:
myGrid.DataContext = new Project();
Working with Images incorrectly will typically cause the crash; Show the code for implementation of your Elements and how you setting the images.
Also your XAML is missing ItemTemplate,where u would set the image and text.
I'd guess it's crashing because you've got FrameworkElements that you've added to a Canvas, but then you're also adding them to your List. FrameworkElements generally don't like being added to the visual tree multiple times.
If this is the problem, something like this might work around it (bind your list to ElementsAsStrings):
private ObservableCollection<FrameworkElement> elements;
public ObservableCollection<FrameworkElement> Elements
{
get { return elements; }
set
{
if(elements != null)
elements.CollectionChanged -= onElementsChanged;
elements = value;
if(elements != null)
elements.CollectionChanged += onElementsChanged;
NotifyPropertyChanged("Elements");
NotifyPropertyChanged("ElementsAsStrings");
}
}
public IEnumerable<string> ElementsAsStrings
{
get
{
foreach(var element in Elements)
{
if(element is TextBox)
yield return (element as TextBox).Text;
// More cases here
}
}
}
private void onElementsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
NotifyPropertyChanged("ElementsAsStrings");
}
I have a scrollviewer with a couple listboxes in it. The problem is if a user uses the middle mouse roller to scroll the scrollviewer while their mouse is over a listview. The listview scrolls its internal scrollviewer to the bottom and then continues to capture the mouse, preventing the containing scrollviewer from scrolling.
Any ideas on how to handle this?
That happens because the ListView's (ListBox's, actually) content template wraps its items with a ScrollViewer by itself.
The simplest way is to disable it by dropping your own Template for the inside ListView, one that doesn't create a ScrollViewer:
<ListView>
<ListView.Template>
<ControlTemplate>
<ItemsPresenter></ItemsPresenter>
</ControlTemplate>
</ListView.Template>
...
</ListView>
BTW the same happens if you have a ListView inside a ListView (this was my case).
IMO, the best way to handle this scenario is to create a custom control :
class MyScrollViewer : ScrollViewer
{
protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
{
base.OnPreviewMouseWheel(e);
if (!e.Handled)
{
e.Handled = true;
this.RaiseEvent(new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = this
});
}
}
}
Did you try disabling the ListView's ScrollBars?
<ListView ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled" />
Inspired by some helpful answers, I have an implementation that scrolls ancestor ScrollViewers when inner ones (including from ListView, ListBox, DataGrid) scroll past their top/bottom.
I apply an attached property to all ScrollViewers in App.xaml:
<Style TargetType="ScrollViewer" BasedOn="{StaticResource {x:Type ScrollViewer}}">
<Setter Property="local:ScrollViewerHelper.FixMouseWheel" Value="True" />
</Style>
The attached property detects scrolling past top/bottom, and when that happens raises a mouse wheel event on the ScrollViewer's parent. Event routing gets it to the outer ScrollViewer:
public static class ScrollViewerHelper
{
// Attached property boilerplate
public static bool GetFixMouseWheel(ScrollViewer scrollViewer) => (bool)scrollViewer?.GetValue(FixMouseWheelProperty);
public static void SetFixMouseWheel(ScrollViewer scrollViewer, bool value) => scrollViewer?.SetValue(FixMouseWheelProperty, value);
public static readonly DependencyProperty FixMouseWheelProperty =
DependencyProperty.RegisterAttached("FixMouseWheel", typeof(bool), typeof(ScrollViewerHelper),
new PropertyMetadata(OnFixMouseWheelChanged));
// End attached property boilerplate
static void OnFixMouseWheelChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var scrollViewer = d as ScrollViewer;
if (scrollViewer == null) return;
scrollViewer.PreviewMouseWheel += (s2, e2) =>
{
var parent = scrollViewer.Parent as UIElement;
bool hitTopOrBottom = HitTopOrBottom(e2.Delta, scrollViewer);
if (parent is null || !hitTopOrBottom) return;
var argsCopy = Copy(e2);
parent.RaiseEvent(argsCopy);
};
}
static bool HitTopOrBottom(double delta, ScrollViewer scrollViewer)
{
var contentVerticalOffset = scrollViewer.ContentVerticalOffset;
var atTop = contentVerticalOffset == 0;
var movedUp = delta > 0;
var hitTop = atTop && movedUp;
var atBottom =
contentVerticalOffset == scrollViewer.ScrollableHeight;
var movedDown = delta < 0;
var hitBottom = atBottom && movedDown;
return hitTop || hitBottom;
}
static MouseWheelEventArgs Copy(MouseWheelEventArgs e)
=> new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = e.Source,
};
}
If you wrap the inner listview in a scrollviewer then the scrolling will work.
<ListView ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListView.ItemTemplate>
<DataTemplate>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<ListView>
<ListView.ItemTemplate>
<DataTemplate>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ScrollViewer>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I'm trying to host the Workflow Designer in a WPF application. The WorkflowView control is hosted under a WindowsFormsHost control. I've managed to load workflows onto the designer which is successfully linked to a PropertyGrid, also hosted in another WindowsFormsHost.
WorkflowView workflowView = rootDesigner.GetView(ViewTechnology.Default) as WorkflowView;
window.WorkflowViewHost.Child = workflowView;
The majority of the rehosting code is the same as in http://msdn.microsoft.com/en-us/library/aa480213.aspx.
I've created a custom Toolbox using a ListBox WPF control bound to a list of ToolboxItems.
<ListBox Grid.Row="1" Margin="0 0 0 4" BorderThickness="1" BorderBrush="DarkGray" ItemsSource="{Binding Path=ToolboxItems}" PreviewMouseLeftButtonDown="ListBox_PreviewMouseLeftButtonDown" AllowDrop="True">
<ListBox.Resources>
<vw:BitmapSourceTypeConverter x:Key="BitmapSourceConverter" />
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type dd:ToolboxItem}">
<StackPanel Orientation="Horizontal" Margin="3">
<Image Source="{Binding Path=Bitmap, Converter={StaticResource BitmapSourceConverter}}" Height="16" Width="16" Margin="0 0 3 0" />
<TextBlock Text="{Binding Path=DisplayName}" FontSize="14" Height="16" VerticalAlignment="Center" />
<StackPanel.ToolTip>
<TextBlock Text="{Binding Path=Description}" />
</StackPanel.ToolTip>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
In the ListBox_PreviewMouseLeftButtonDown handler:
private void ListBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
ListBox parent = (ListBox)sender;
UIElement dataContainer;
//get the ToolboxItem for the selected item
object data = GetObjectDataFromPoint(parent, e.GetPosition(parent), out dataContainer);
//if the data is not null then start the drag drop operation
if (data != null)
{
DataObject dataObject = new DataObject();
dataObject.SetData(typeof(ToolboxItem), data);
DragDrop.DoDragDrop(parent, dataObject, DragDropEffects.Move | DragDropEffects.Copy);
}
}
With that setup, I'm unable to drag any item from my custom Toolbox onto the designer. The cursor is always displayed as "No" anywhere on the designer.
I've been trying to find anything about this on the net for half a day now and I really hope some can help me here.
Any feedback is much appreciated. Thank you!
Carlos
This might sound stupid as my system is shutting down. :) But can you check your WorkflowView for whether AllowDrop is set? Have you handled the DragEnter event?
Finally got Drag and Drop working. There were three things that needed doing, for whatever reason WorkflowView has:
1.) I had to use System.Windows.Forms.DataObject instead of System.Windows.DataObject when serializing the ToolboxItem when doing DragDrop.
private void ListBox_MouseDownHandler(object sender, MouseButtonEventArgs e)
{
ListBox parent = (ListBox)sender;
//get the object source for the selected item
object data = GetObjectDataFromPoint(parent, e.GetPosition(parent));
//if the data is not null then start the drag drop operation
if (data != null)
{
System.Windows.Forms.DataObject dataObject = new System.Windows.Forms.DataObject();
dataObject.SetData(typeof(ToolboxItem), data as ToolboxItem);
DragDrop.DoDragDrop(this, dataObject, DragDropEffects.Move | DragDropEffects.Copy);
}
}
2.) DragDrop.DoDragDrop source must be set to the IToolboxService set in the IDesignerHost. The control holding the ListBox implements IToolboxService.
// "this" points to ListBox's parent which implements IToolboxService.
DragDrop.DoDragDrop(this, dataObject, DragDropEffects.Move | DragDropEffects.Copy);
3.) The ListBox should be bound to a list of ToolboxItems returned by the following helper method, passing it the Type of the activities to show in the tool box:
...
this.ToolboxItems = new ToolboxItem[]
{
GetToolboxItem(typeof(IfElseActivity))
};
...
internal static ToolboxItem GetToolboxItem(Type toolType)
{
if (toolType == null)
throw new ArgumentNullException("toolType");
ToolboxItem item = null;
if ((toolType.IsPublic || toolType.IsNestedPublic) && typeof(IComponent).IsAssignableFrom(toolType) && !toolType.IsAbstract)
{
ToolboxItemAttribute toolboxItemAttribute = (ToolboxItemAttribute)TypeDescriptor.GetAttributes(toolType)[typeof(ToolboxItemAttribute)];
if (toolboxItemAttribute != null && !toolboxItemAttribute.IsDefaultAttribute())
{
Type itemType = toolboxItemAttribute.ToolboxItemType;
if (itemType != null)
{
// First, try to find a constructor with Type as a parameter. If that
// fails, try the default constructor.
ConstructorInfo ctor = itemType.GetConstructor(new Type[] { typeof(Type) });
if (ctor != null)
{
item = (ToolboxItem)ctor.Invoke(new object[] { toolType });
}
else
{
ctor = itemType.GetConstructor(new Type[0]);
if (ctor != null)
{
item = (ToolboxItem)ctor.Invoke(new object[0]);
item.Initialize(toolType);
}
}
}
}
else if (!toolboxItemAttribute.Equals(ToolboxItemAttribute.None))
{
item = new ToolboxItem(toolType);
}
}
else if (typeof(ToolboxItem).IsAssignableFrom(toolType))
{
// if the type *is* a toolboxitem, just create it..
//
try
{
item = (ToolboxItem)Activator.CreateInstance(toolType, true);
}
catch
{
}
}
return item;
}
GetToolboxItem method is from http://msdn.microsoft.com/en-us/library/aa480213.aspx source, in the ToolboxService class.
Cheers,
Carlos