Jaime Rodriguez On Windows Store apps, Windows Phone, HTML and XAML
Today I spent two hours in Cider beta2.. I was not sure what to expect, I must admit in the past I had just disabled it cause I did not enjoy waiting for the "whoops" page..
Here is my first [raw,unedited, un-sugar-coded] impression...
So why the post if it is not all perfect?? Because the time for feedback is now!! If you have projects that the parser can't handle, let the cider team know .. Here is the proposal/offer ... If for what ever reason you don't want to download the VPC or the VS2008 DVD, drop me an email w/ your shipping address and I will ship you a DVD with a VPC -- offer limited to first 50 projects, maybe more pending your patience and your project size --...
I will come back to Cider with a more detailed review next week... but wanted to encourage you all to try it and provide the feedback...
I often skip the "xyz" has shipped announcements.. but VS2008 "orcas" beta2 is packed with so much goodness that I had to jump in to vouch for it.. Check out Soma/ScottGu's video on beta2 http://channel9.msdn.com/showpost.aspx?postid=329443... Start downloading beta2 while you watch the video, cause by the end no chance you will not be interested in playing with it..
I am getting late into it cause I was on vacation last two weeks but here is what I see so far:
Still here? What r u waiting for.. Please go download it and have lots of fun... do file bugs when you find them..
Here is where we take every thing learned in Part1 (the drag) and Part2 (the drop) and package it into more realistic scenarios...
Let's first begin by walking through a few items we skipped in earlier parts for brevity:
When you start a drag, you should check if Ctrl or Shift are pressed... Each one of these being pressed implies a different action for our drag operation...
DragDropEffects effects = DragDropEffects.None; bool ctrl = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl); bool shift = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); if (ctrl && shift && AllowsLink) effects |= DragDropEffects.Link; else if (ctrl && AllowsCopy) effects |= DragDropEffects.Copy; else if (AllowsMove) // the default is to move in my sample .. effects |= DragDropEffects.Move;
Deciding what needs to be dragged is sucky because you almost always have to put app knowledge into your drag & drop code.. There is not a one size fits all..
For example: <Button Content="Test" /> will generate a tree that looks like: Button - > Chrome - > Content Presenter - > TextBlock ... So, if you hitTest on what is visible, you might get the TextBlock when what you wanted was the button ....
I don't have a great answer for this. As I mentioned, my approach is to lazily shoot for the OriginalSource element in the MouseMove event handler.. This seems to work slightly better than doing HitTesting (where I could get an item lower in the tree like the TextBlock ..
For those that do want to do hittesting, the code is trivial:
UIElement GetDragElementFromHitTest(UIElement dragSourceContainer, MouseEventArgs args) { HitTestResult hr = VisualTreeHelper.HitTest( dragSourceContainer, args.GetPosition((IInputElement) dragSourceContainer)); return hr.VisualHit as UIElement; }
I have to emphasize d&d was meant for data.. In the demo below I make the mistake of letting you drag UIElements and the like.. This is for illustration purposes... imho if you find yourself trying to drag any thing other than data, you should evaluate decoupling presentation and data..
What I have done for my demos is create a library with:
I have also defined an interface that a class can implement to aid in the dragOperation...
public interface IDataDropObjectProvider { //Flag of actions sypported by implementation of IDataDropObjectProvider DragDropProviderActions SupportedActions { get; } // Called before StartDrag () to get the Data () to be used in the DataObject object GetData(MouseEventArgs e); // Called to get the visual ( UIElement visual brush of the object being dragged.. UIElement GetVisual(MouseEventArgs e); // Gives feedback during Drag void GiveFeedback(GiveFeedbackEventArgs args); // implements ContinueDrag -- to canceld the D&D.. void ContinueDrag(QueryContinueDragEventArgs args); // called by the TARGET object .. this will attempt to "unparent" the current child so we can add it a child some where else.. bool UnParent(); }
This interface gets called back from either DragHelper or Drophelper (whenever needed)
A few things to notice:
the interface is not necessarily implemented by either the source or the target. it is de-coupled from both. Any other class can implement it and it can be generic.
The interface is optional, if you do not implement it, the helpers classes will do their best to help.. My advise is always implement the GetData ( ) part of the interface.
You can implement just part of the interface. DragDropProviderActions is a flag that says what actions the interface supports..
Is pretty trivial ... the constructor for DragHelper looks like this:
public DragHelper(UIElement source, IDataDropObjectProvider callback, UIElement dragScope)
Here are examples of using the constructors... ( DropHelper I did not explain above beccuse the only parameter in is the drop target control itself..
dragHelper = new DragHelper(this.canvas, null, null); dropHelper = new DropHelper(this.dropTargetPanel1 ); dragHelperWScope = new DragHelper(this.canvas1, null, (UIElement)this.BorderForScope); dropHelperWScope = new DropHelper(this.dropTargetPanel2);
For the most part, these classes do what we discussed .. the exception is the drop..
Drop of data is straight forward... there is no generic recipe for all scenarios but two that I use often are: dropping to ContentControl or dropping to ItemsControl.. in which case I set appropriate property or add the data to the items collection ..
Drop of UIElements is slightly trickier mostly because you need to re-parent your element to the target.. . The sample classes use the IDataDropObjectProvider to Unparent () the UIElement before dropping it..
Note that this assumes every thing is in process, the interface passes UIElements and other stuff around ... To implement an Out of process handler for passing UI, you can make COM objects and pass that around..
Is not pretty ... but quite functional. .. Here is the first half ..
The second half of the app is even uglier, it is in Window2.xaml but it is a common scenario...
This does drag & drop across listboxes.. it uses Data, but provides a Visual Adorner for feedback... The things to notice is that it implements the IDataDropObjectProvider interface ... so you can see how that works.. You can click one of the planets and drag into Word or other external app that allows for text drops to get the XML representation of the planet..
The listbox that you are dropping into does not allow duplicates.. so don't try dropping same thing twice.. it will quietly smirk at you..
OK that covers my D&D walk through ... The source for the sample in part 3 is here..
{let me know if you find bugs or issues }
Last part, we focused on the drag... In this part we will focus on drop (which is relatively straight forward) ...
It all begins with a Visual Element setting its AllowDrop to true; this tells the D&D subsystem your control is a drop target ... You can do this in XAML or in code..
I hardly ever subscribe to any of these events.. but let's walk through the niceties on each ..
From our previous work in progress project, I replaced old DropTarget StackPanel with a DropTargetText and DropTargetImages ... DropTargetImages will handle DragOver,DragEnter, Dragleave to check if dataObject type is Images and if not, it will set e.Effects to None, and provide some ugly feedback.. DropTargetText will handle Dragover and Drop (since our old dataobject type is Text)
void Window1_Loaded(object sender, RoutedEventArgs e) { this.DragSource.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(DragSource_PreviewMouseLeftButtonDown); this.DragSource.PreviewMouseMove += new MouseEventHandler(DragSource_PreviewMouseMove);
// DROP this.DropTargetImages.DragEnter += new DragEventHandler(DropTargetImages_DragEnter); this.DropTargetImages.DragLeave += new DragEventHandler(DropTargetImages_DragLeave); this.DropTargetImages.DragOver += new DragEventHandler(DropTargetImages_DragOver); this.DropTargetText.DragOver += new DragEventHandler(DropTargetText_DragOver); this.DropTargetText.Drop += new DragEventHandler(DropTargetText_Drop);
}
void DropTargetText_Drop(object sender, DragEventArgs e) { IDataObject data = e.Data;
if (data.GetDataPresent(DataFormats.Text)) { MessageBox.Show( string.Format("right format, thanks for dropping '{0}'" , ((string)data.GetData(DataFormats.Text)))); }
void DropTargetImages_DragOver(object sender, DragEventArgs e) { if (!e.Data.GetDataPresent("Images")) { e.Effects = DragDropEffects.None; e.Handled = true; } }
void DropTargetText_DragOver(object sender, DragEventArgs e) { if (!e.Data.GetDataPresent("Text")) {
e.Effects = DragDropEffects.None; e.Handled = true; } }
void DropTargetImages_DragLeave(object sender, DragEventArgs e) { RunStoryboard("Timeline2");
void RunStoryboard(string name) { Storyboard sb = this.FindResource(name) as Storyboard ; System.Diagnostics.Debug.Assert(sb != null); sb.Begin(DropTargetImages);
void DropTargetImages_DragEnter(object sender, DragEventArgs e) { if (!e.Data.GetDataPresent("Image")) { RunStoryboard("Timeline1");
e.Effects = DragDropEffects.None; e.Handled = true; }
The source code for snippet above is here.
Handling the Drop can be that straight forward, but it almost never is that simple..
In Part3, I can walk through much more samples of handling drop ...
How to do Drag& Drop in WPF is a question I hear often... I have seen some great samples out there, but most focus on either a big code sample or a niche scenario... a couple of times I have ended up having to help some one who got stuck. I hope the below write up is useful to explain the steps and decisions to get drag & drop done.. and it comes with sample snippets ...
-- ---- -------------------From [http://msdn2.microsoft.com/en-us/library/aa289508(vs.71).aspx] Here is the sequence of events in a typical drag-and-drop operation:
The DoDragDrop method takes two parameters:
A new DataObject object is automatically created.
------------
Before the DoDragDrop is called, we must detect a mouse Drag operation on the source... A mouse drag is usually a MouseLeftButtonDown + a MouseMove (before MouseLeftButton goes up) ...
So, our drag & drop source control needs to subscribe to these two events:
void Window1_Loaded(object sender, RoutedEventArgs e) { this.DragSource.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(DragSource_PreviewMouseLeftButtonDown); this.DragSource.PreviewMouseMove += new MouseEventHandler(DragSource_PreviewMouseMove); }
To prevent from starting a false drag & drop operation where the user accidentally drags, you can use SystemParameters.MinimumHorizontalDragDistance and SystemParameters.MinimumVerticalDragDistance
One way to do this is on MouseLeftButtonDown, record the starting position and onMouseMove check if the mouse has moved far enough..
void DragSource_PreviewMouseMove(object sender, MouseEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed && !IsDragging) { Point position = e.GetPosition(null); if (Math.Abs(position.X - _startPoint.X) > SystemParameters.MinimumHorizontalDragDistance || Math.Abs(position.Y - _startPoint.Y) > SystemParameters.MinimumVerticalDragDistance) { StartDrag(e); } } } void DragSource_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { _startPoint = e.GetPosition(null); }
The data! You need to find out what is under the mouse when dragging. I will omit take the easy way out and assume that whoever is triggering the MouseMove is what I want to drag .. so look at MouseEventArgs.OriginalSource.. [or you could do some 2D HitTesting using VisualTreeHelper .. In Part3 of this write up will try to walk you through hit testing the listbox -which is the other common scenario I encounter-.
Once you have the object to drag, you will need to package what you are a sending into a DataObject that describes the data you are passing around. DataObject is a wrapper to push generic data (identified with extensible formats) into drag/drop.. As long as both the source and destination understand the format, you will be set. As such, DataObject has a couple interesting methods:
Not much interesting stuff here.. In the sample I just hard-coded my data to be of type string... this makes it easier to paste into external containers (for example Word, which you can use to test this part of the write-up). I do have to stress that drag & dropping should be about the data ...
Before we call DoDragDrop () we have a few 'choices' to make around the feedback we want to provide and the 'scope' of the d&d.
If you don't want a fancy cursor, you are done!! You can call DoDragDrop directly ...
private void StartDrag(MouseEventArgs e) { IsDragging = true; DataObject data = new DataObject(System.Windows.DataFormats.Text.ToString(), "abcd"); DragDropEffects de = DragDrop.DoDragDrop(this.DragSource, data, DragDropEffects.Move); IsDragging = false; }
Note: this code allows you to drag & drop across processes, it uses the default operating system feedback ( e.g. + for copy)..
Say we had a .cur file and embedded it on to our application as a resource ( see sample code). We can subscribe to GiveFeedback () and wire our cursor there..
private void StartDragCustomCursor(MouseEventArgs e) { GiveFeedbackEventHandler handler = new GiveFeedbackEventHandler(DragSource_GiveFeedback); this.DragSource.GiveFeedback += handler; IsDragging = true; DataObject data = new DataObject(System.Windows.DataFormats.Text.ToString(), "abcd"); DragDropEffects de = DragDrop.DoDragDrop(this.DragSource, data, DragDropEffects.Move); this.DragSource.GiveFeedback -= handler; IsDragging = false; }
Our handler for feedback looks like this:
void DragSource_GiveFeedback(object sender, GiveFeedbackEventArgs e) { try { //This loads the cursor from a stream .. if (_allOpsCursor == null) { using (Stream cursorStream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream( "SimplestDragDrop.DDIcon.cur")) { _allOpsCursor = new Cursor(cursorStream); } } Mouse.SetCursor(_allOpsCursor); e.UseDefaultCursors = false; e.Handled = true; } finally { } }
Two things to notice: 1) I cached the cursor... GiveFeedback will be called many times as the mousemoves so I cached it.. and
2) though I did not handle it, I called it "_allOpsCursor" because GiveFeedbackEventArgs will tell you the possible operation for the cursor (e.Effects)... I could have used multiple cursors, one for each effect.
The first thing you will need is to an Adorner; in my case I chose and adorner that contains a VisualBrush of the Element being dragged... you can go with RenderTargetBitmap, or possibly reparent the object directly ... but I like VisualBrush in case the drag is cancelled..
The constructor for the adorner class is where most of the action happens:
public DragAdorner(UIElement owner, UIElement adornElement, bool useVisualBrush, double opacity) : base(owner) { _owner = owner; if (useVisualBrush) { VisualBrush _brush = new VisualBrush(adornElement); _brush.Opacity = opacity; Rectangle r = new Rectangle(); r.RadiusX = 3; r.RadiusY = 3; r.Width = adornElement.DesiredSize.Width; r.Height = adornElement.DesiredSize.Height; XCenter = adornElement.DesiredSize.Width / 2; YCenter = adornElement.DesiredSize.Height / 2; r.Fill = _brush; _child = r; } else _child = adornElement; }
//There is more code in DragAdorner, but mostly used for positioning the adorner as the drag is happening... please refer to the sample...
Now, that we have our custom adorner ready, the tricky part is wiring it so it follows the cursor position. There are two options here:
First we have to import Win32's code using DllImport .... [trivial stuff, refer to sample code in Win32.cs ]
Next we create an instance of a Window, which will contain a visual brush of the element we are dragging ...
private Window _dragdropWindow = null; private void CreateDragDropWindow(Visual dragElement) { System.Diagnostics.Debug.Assert(this._dragdropWindow == null); System.Diagnostics.Debug.Assert(dragElement != null); // TODO: FE? or UIE?? FE cause I am lazy on size . System.Diagnostics.Debug.Assert(dragElement is FrameworkElement); this._dragdropWindow = new Window(); _dragdropWindow.WindowStyle = WindowStyle.None; _dragdropWindow.AllowsTransparency = true; _dragdropWindow.AllowDrop = false; _dragdropWindow.Background = null; _dragdropWindow.IsHitTestVisible = false; _dragdropWindow.SizeToContent = SizeToContent.WidthAndHeight; _dragdropWindow.Topmost = true; _dragdropWindow.ShowInTaskbar = false; _dragdropWindow.SourceInitialized += new EventHandler( delegate(object sender, EventArgs args) { //TODO assert that we can do this.. PresentationSource windowSource = PresentationSource.FromVisual(this._dragdropWindow); IntPtr handle = ((System.Windows.Interop.HwndSource)windowSource).Handle; Int32 styles = Win32.GetWindowLong(handle, Win32.GWL_EXSTYLE); Win32.SetWindowLong(handle, Win32.GWL_EXSTYLE, styles | Win32.WS_EX_LAYERED | Win32.WS_EX_TRANSPARENT); }); Rectangle r = new Rectangle(); r.Width = ((FrameworkElement)dragElement).ActualWidth; r.Height = ((FrameworkElement)dragElement).ActualHeight; r.Fill = new VisualBrush(dragElement); this._dragdropWindow.Content = r; // put the window in the right place to start UpdateWindowLocation(); }
Notice:
1) I set the style to Transparent, layered window (this is ok since the window is small and it is only used for drag & drop ).. and
2) the call to UpdateWindowLocation () this is the code that positions the Window wherever the cursor is now..
3) I likely need more error checking
The code in UpdateWindowLocation is straight forward:
void UpdateWindowLocation() { if (this._dragdropWindow != null) { Win32.POINT p; if (!Win32.GetCursorPos(out p)) { return; } this._dragdropWindow.Left = (double)p.X; this._dragdropWindow.Top = (double)p.Y; } }
This UpdateLocation code of course needs to be called whenever the cursor moves... so we need some kind of callback during the drag operation.. We will use QueryContinueDrag for that..
So, I go back to the code in StartDrag () and wire up the event, as well as some code to show the window and destroy it after drag & drop:
private void StartDragWindow(MouseEventArgs e) { GiveFeedbackEventHandler feedbackhandler = new GiveFeedbackEventHandler(DragSource_GiveFeedback); ; this.DragSource.GiveFeedback += feedbackhandler; QueryContinueDragEventHandler queryhandler = new QueryContinueDragEventHandler(DragSource_QueryContinueDrag); this.DragSource.QueryContinueDrag += queryhandler; IsDragging = true; CreateDragDropWindow(this.dragElement); DataObject data = new DataObject(System.Windows.DataFormats.Text.ToString(), "abcd"); this._dragdropWindow.Show(); DragDropEffects de = DragDrop.DoDragDrop(this.DragSource, data, DragDropEffects.Move); DestroyDragDropWindow(); IsDragging = false; this.DragSource.GiveFeedback -= feedbackhandler; this.DragSource.QueryContinueDrag -= queryhandler; }
The one thing to notice is that I still have GiveFeedbackHandler wired.. Why ?? We are no longer using the cursor... but we still have to tell Drag & Drop not to use the default cursors..
There is a slightly different approach you can use if you are drag & dropping just inside your app or have a smaller scope ... I some times use this approach because it allows me to avoid interop, avoid creating extra windows, and better control the scope of the drag...
Here is the full explanation of how it works and why it feels like hackalicious.
When you call DoDragDrop, there is no Mouse or Cursor Events being fired in your WPF app... OLE does the work for you and it moves cursor directly :(... however, all of the Drag events are being fired...
We already know of the two events we can tap into from the source: GiveFeedback and QueryContinueDrag... however neither of these events gives us access to the mouse or cursor position during the drag operation :( ... We can however tap into the Dragover event; DragOverEventArgs has a GetPosition ( ) method that does the trick... DragOver however is fired in the target, not the source.
So, how would we do it?? Well , DragEvents are routed events.. they bubble up.. if we define a "Drag Scope" within our app that we know is guaranteed to bubble the DragOver, then we can listen for it ... the obvious choice for that scope is our Application's Window; this gives us access to any thing in our app; the scope could be smaller of course...
Here is how we wire that:
private void StartDragInProcAdorner(MouseEventArgs e) { // Let's define our DragScope .. In this case it is every thing inside our main window .. DragScope = Application.Current.MainWindow.Content as FrameworkElement; System.Diagnostics.Debug.Assert(DragScope != null); // We enable Drag & Drop in our scope ... We are not implementing Drop, so it is OK, but this allows us to get DragOver bool previousDrop = DragScope.AllowDrop; DragScope.AllowDrop = true; // Let's wire our usual events.. // GiveFeedback just tells it to use no standard cursors.. GiveFeedbackEventHandler feedbackhandler = new GiveFeedbackEventHandler(DragSource_GiveFeedback); this.DragSource.GiveFeedback += feedbackhandler; // The DragOver event ... DragEventHandler draghandler = new DragEventHandler(Window1_DragOver); DragScope.PreviewDragOver += draghandler; // Drag Leave is optional, but write up explains why I like it .. DragEventHandler dragleavehandler = new DragEventHandler(DragScope_DragLeave); DragScope.DragLeave += dragleavehandler; // QueryContinue Drag goes with drag leave... QueryContinueDragEventHandler queryhandler = new QueryContinueDragEventHandler(DragScope_QueryContinueDrag); DragScope.QueryContinueDrag += queryhandler; //Here we create our adorner.. _adorner = new DragAdorner(DragScope, (UIElement)this.dragElement, true, 0.5); _layer = AdornerLayer.GetAdornerLayer(DragScope as Visual); _layer.Add(_adorner); IsDragging = true; _dragHasLeftScope = false; //Finally lets drag drop DataObject data = new DataObject(System.Windows.DataFormats.Text.ToString(), "abcd"); DragDropEffects de = DragDrop.DoDragDrop(this.DragSource, data, DragDropEffects.Move); // Clean up our mess :) DragScope.AllowDrop = previousDrop; AdornerLayer.GetAdornerLayer(DragScope).Remove(_adorner); _adorner = null; DragSource.GiveFeedback -= feedbackhandler; DragScope.DragLeave -= dragleavehandler; DragScope.QueryContinueDrag -= queryhandler; DragScope.PreviewDragOver -= draghandler; IsDragging = false; }
Explanations:
There is a common gotcha with the DragLeave part of this scenario. The scope tends to always be a panel,grid, etc.. ( a container) and if the container has background to null, it is not hittesting, so you won't get the dragleave... You have to explicitly set the Background="Transparent" to make sure you get it... (you can see it in my sample with the Grid)..
That is it for Drag ... I hope I explained how to do the Drag part of a drag & drop. I want to cut part 1 here so that you have a pretty clean sample of the "drag" ..
The source for every thing above is here.
You will have to tweak the MouseMove function to select which drag approach to use.. Just make sure you have at most one of these functions uncommented at any time..
// StartDrag(e);// StartDragCustomCursor(e);// StartDragWindow(e);StartDragInProcAdorner(e);
Since I did not wire a Drop handler, for testing this, just "Drop" into some thing that handles Text like Microsoft Word..
In part 2, I will cover the drop .. and in part 3 I will share the complete code with a couple of extra things that I omitted here to try to keep it clean (some of them might be in the code sample)
From an internal DL ... I thought this would be handy --- must admit is nicer than some hacks I have done in the past to accomplish same thing..
---
Sent: Tuesday, July 03, 2007 2:00 PMSubject: RE: forcing a rebuild of an items control...
You’ll need to know the dependency object and dependency property where the binding was defined, which may not qualify as elegant. Then you can use this line:
BindingOperations.GetBindingExpressionBase(dependencyObject, dependencyProperty).UpdateTarget();
--Eric
Sent: Tuesday, July 03, 2007 1:08 PMSubject: forcing a rebuild of an items control...
I have a control that has a list of stuff, and once I make a change to some unrelated thing I really need to force the binding to re-run… is there any elegant way to make this happen? Something like “InvalidateMeasure” but for binding… ?
When working on Silverlight 1.0 projects.. it kind of annoyed me that Blend launches notepad to edit my JS file... I tried going into Windows and tell it to always "open" JS files with Visual Studio... (via OpenWith in the shell ) ... but Blend kept launching notepad.. why??
The shell associates the "open" command with VS .. that was working fine.. but blend used the "Edit" command instead of "open" ... So, I had to configure Windows to "edit" .JS files with Visual Studio by editing the registry entry at HKEY_CLASSES_ROOT\JSFile\Shell\Edit\Command\(Default), the command is set to Notepad and I had to edit the registry entry and change it to point to devenv.exe: C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe [for a default location VS 2005 install] or C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\devenv.exe [for a default location VS 2008 "Orcas" installed" ] ...
Now, when I double click on a JS file in Blend May Preview, it launches VS ...