Jeff Simon's Blog

  • Uh oh, I've been Crevier'ed!

    It's like being \.'ed, only with less penguins.

    http://blogs.msdn.com/dancre/archive/2006/02/22/537534.aspx

    He's also got an update to his LayoutAnimation series with a pretty cool new technique - I'll have to consider updating my codebase around his idea.

     

  • What A Drag...

    Not to sound too much like a fanboy, but Avalon just plain rocks.  I’ve spent the last fifteen years simultaneously cursing (“I hate you!”) and coercing (“…but I’ll love you if you work!”) GDI and User into doing my evil bidding, and I’ve finally found a replacement that I actually want to use.  Lord knows there’s been no dearth of presentation layers in the interim, but none of them caught my eye; either they didn’t have the full functionality I wanted, or they just plain felt heavy.  Avalon, though, really fits the bill (ok, yes, it’s heavy, but for whatever reason it doesn’t feel that way).  Especially now while I’m still learning about it, there are lots of ‘ah, suh-weet’ moments as I find that the pain of programming has been reduced.

     

    Aaaaand then there’s Drag and Drop.

     

    For whatever reason - perhaps there are other more important things to focus on shipping, or perhaps the old way was “good enough” – Avalon’s Drag & Drop implementation is simply a very thin wrapper around the same OLE drag & drop that I’ve been scratching my head over for years.  You can see this for yourself by using the excellent Reflector .Net class browser and looking at the System.Windows.DragDrop implementation.

     

    On the plus side this means that, unlike with the rest of Avalon (aside from the indispensible O’Reilly book), there is an absolute glut of information available discussing how to do Drag & Drop.

     

    On the down side though, this means that it’s still challenging to do anything other than the most basic Drag&Drop implementation.  Inter-app support, custom cursors, multi-drop-target scenarios, and custom drag windows require a little more effort than the rest of Avalon implies should be necessary.

     

    Given some of the visual cues I’ll be adding to the DragDropTilePanel, the inter-app support (e.g., between two different instances of your app) provides the biggest challenge, as it means that we have to fully decouple our DragSource and DropTarget implementations. For many developers, such as those working on prototype, beta, or constrained-scenario apps, disallowing inter-app drag & drop is an acceptable limitation; that said, we really should write it correctly so that the drag source and drop target don’t share implementation.

     

    That said; I’m going to wuss out for the next couple of posts and describe a simple Drag&Drop implementation that ignores inter-app support and clean factoring of drag source and drop target.  Hopefully Dan and Ben won’t read this and give me grief about taking the easy way out…

     

    Here’s the minimum code necessary to add Drag & Drop support to the TilePanel base class. On the plus side, this is very little code!  Also, notice that even though we’re not doing anything related to animation here, all of the child objects smoothly animate to their new positions after a drop – all thanks to the animation code that Dan originally implemented.

     

        public class DragDropTilePanel : TilePanel

        {

            protected override void OnInitialized(EventArgs e)

            {

                // Add mouse event handlers to each of our child objects

                foreach (FrameworkElement child in Children)

                {

                    child.MouseLeftButtonDown += new MouseButtonEventHandler(child_MouseLeftButtonDown);

                    child.MouseMove += new MouseEventHandler(child_MouseMove);

                }

                base.OnInitialized(e);

            }

     

    The code above sets up the mouse event handlers that inform us when the user interacts with one of the TilePanel’s child objects.

     

            void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs eventArgs)

            {

                // User pressed the mouse button on one of our draggable children.  Don't start dragging

                // just yet, but do track where the mouse was when the button was pressed.

                FrameworkElement childElement = (FrameworkElement)sender;

                _mouseDownLocationRelativeToChild = eventArgs.GetPosition(childElement);

            }

     

    We don’t start dragging until the user’s moved a sufficient distance from where they pressed the mouse button, so don’t start the drag in the MouseDown handler…

     

            void child_MouseMove(object sender, MouseEventArgs eventArgs)

            {

                if (eventArgs.LeftButton == MouseButtonState.Pressed)

                {

                    FrameworkElement draggedChildElement = (FrameworkElement)sender;

     

                    // The Mousebutton is pressed, but we haven't started dragging yet; check

                    // to see if we should.  Only do so if we've moved far enough away from

                    // the initial mouse-down location. 

                    Point currentMouseLocationRelativeToChild = eventArgs.GetPosition(draggedChildElement);

                    if (!PointIsOutsideDragThreshold(currentMouseLocationRelativeToChild))

                    {

                        // We're not ready to start dragging quite yet 'cause the user hasn't

                        // moved the mouse far enough.

                        return;

                    }

                    // Fade out the dragged item to give visual feedback that it's being moved.

                    draggedChildElement.Opacity = 0.5f;

     

                    // Create the DataObject that will contain the dragged child. This won’t work

                    // When dropped on another app (since it won’t be able to deref the DraggedObject)

                    DataObject dataObject = new DataObject();

                    dataObject.SetData("DraggedObject", draggedChildElement);

     

                    // Do the actual drop.

                    DragDrop.DoDragDrop(this, dataObject, DragDropEffects.Copy | DragDropEffects.Move);

     

                    // Restore the object's opacity.

                    draggedChildElement.Opacity = 1.0f;

                }

            }

     

    The code above belongs to the ‘drag source’ – the object which instantiates the drag event.  It first checks to ensure that the user has moved the mouse far enough to trigger the Drag; once that’s happened, it sets up the Drag by wrapping the dragged child element and adding it to the dataObject (which you can think of as a varargs passed to the Drag event handlers).  Note that, to support inter-app drag&drop, we’d need to ensure that all data we store in dataObject is serializable (or, I suppose, we could use Remoting, but that’s more of a black box to me right now).  If we try to pass an object like a FrameworkElement, then the OnDrop handler, which could well be called in another app, would fail as it tried to get that data from the DataObject.

     

    Finally, it calls DoDragDrop.  This function call is synchronous – it doesn’t return until the drag event is completed.

     

            protected override void OnDrop(DragEventArgs dragArgs)

            {

                FrameworkElement childElement = (FrameworkElement)dragArgs.Data.GetData("DraggedObject");

     

                // Get the current mouse position, and add half-an-object-width to it so that

                // the drop position is more natural when an object is dropped onto another object.

                // (drop onto the left half of an object, it'll insert before that one; drop onto

                // the right half of an object, it'll insert after it).

                Point currentMousePosition = dragArgs.GetPosition(this);

                Point halfOffsetMousePosition = new Point(currentMousePosition.X + ChildSize / 2, currentMousePosition.Y);

     

                // Move the object to the drop point.  Set the childElement's DataContext

                // to the object's new index in the list (caculated from the mouse position)

                Children.Remove(childElement);

                int dropIndex = GetChildIndexFromPosition(halfOffsetMousePosition);

                Children.Insert(dropIndex, childElement);

                childElement.DataContext = dropIndex;

            }

     

    The OnDrop code above belongs to the “drop target” – the object on which the dragged object was dropped.  As described above, for our purposes here we’re assuming that OnDrop is in the same object (and same object instance) as the DoDragDrop call.

     

            internal bool PointIsOutsideDragThreshold(Point currentMouseLocationRelativeToChild)

            {

                // Use the SystemParameter's drag distances and create a rectangle centered

                // around the mousedown position - check to see if the current mouse position

                // is outside that rectangle; if so, start a'draggin.

                double horizontalDragThreshold = SystemParameters.MinimumHorizontalDragDistance;

                double verticalDragThreshold   = SystemParameters.MinimumVerticalDragDistance;

                Rect dragThresholdRect = new Rect(_mouseDownLocationRelativeToChild.X - horizontalDragThreshold,

                                                  _mouseDownLocationRelativeToChild.Y - verticalDragThreshold,

                                                  horizontalDragThreshold * 2,

                                                  verticalDragThreshold * 2);

                return !dragThresholdRect.Contains(currentMouseLocationRelativeToChild);

            }

           

            Point _mouseDownLocationRelativeToChild;

        }

     

    The function (and Point variable) above are used to determine if the mouse has moved outside the drag threshold.

     

    There are a few other minor tweaks to round out our Drag support.  First, we add the GetChildIndexFromPosition support function to TilePanel.  This could have gone in DragDropTilePanel, but it could find other uses in TilePanel as well.  Here’s that code.

     

        protected int GetChildIndexFromPosition(Point position)

        {

            int row = (int)(position.Y / _oldChildSize);

            int column = Math.Min(_oldChildrenPerRow, (int)(position.X / _oldChildSize));

            int index = row * _oldChildrenPerRow + column;

            return Math.Min(index, Children.Count);

        }

     

    The ‘Math.Min’s are there to deal with cases where the user drops after the end of a row.

     

    And then finally, we need to tweak the xaml slightly to support our new DragDropTilePanel:


     

    <?Mapping XmlNamespace="MyApp" ClrNamespace="BasicDragDrop" ?>

    <Window x:Class="BasicDragDrop.Window1"

        xmlns="http://schemas.microsoft.com/winfx/avalon/2005"

        xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"

        xmlns:myapp="MyApp"

        Title="BasicDragDrop"

        Height="300"

        Width="300">

      <Grid>

        <RowDefinition Height="24"/>

        <RowDefinition Height="*"/>

        <Slider MinWidth="200" Minimum="50" Maximum="300"

                SmallChange="40" Name="_slider" Grid.Row="0"/>

        <!-- Note: it's important to have a background in the TilePanel,

               otherwise the OnDragOver call won't get to it! Not sure why... -->

        <myapp:DragDropTilePanel Background="White" Grid.Row="1"

                                 ChildSize="{Binding ElementName=_slider, Path=Value}">

          <Border Background="Red" Margin="4" />

          <Border Background="Green" Margin="4" />

          <Border Background="Blue" Margin="4" />

          <Border Background="Red" Margin="4" />

          <Border Background="Green" Margin="4" />

          <Border Background="Blue" Margin="4" />

          <Border Background="Red" Margin="4" />

          <Border Background="Green" Margin="4" />

          <Border Background="Blue" Margin="4" />

          <Border Background="Red" Margin="4" />

          <Border Background="Green" Margin="4" />

          <Border Background="Blue" Margin="4" />

        </myapp:DragDropTilePanel>

      </Grid>

    </Window>

     

    Most of the changes are in converting from a StackPanel to a Grid so that the TilePanel completely fills the remaining window space.  We also specify “AllowDrop” here to tell Avalon that we’re a valid drop target (otherwise we wouldn’t get drop events).  Also, note that for whatever reason, we need to specify a background to our tilePanel, otherwise our OnDragOver doesn’t get called (we don’t have OnDragOver in the DragDropTilePanel implementation above, but we will soon enough, and this will prob’ly catch us, so we’ll add it now). 

    [Edit: Yeah, I realized I should be using a DockPanel instead of a Grid.  I'll rewrite it...]


    And that’s it.  All in all, not a lot of code to add basic drag and drop support that works with any child object type!  Of course, I’m really punting on some key issues (especially that inter-app bit).  We’ll clean that up soon enough.

     

    Updated project file attached (built against the Dec CTP.  Compile against other versions at your own risk).

     

    Next up: Custom drag windows.

     

  • The Aha moment

    Every Sunday, my wife and I head down to Starbucks with the Sunday paper and a red felt-tip pen, and tackle the New York Times crossword puzzle (or, if we’re particularly sleep-deprived because of our baby boy, the Seattle Times crossword – yes, I feel guilty every time we “wimp out”).  Each time I open the paper, I’m hoping to see something different in the puzzle; mysterious circles in the squares, an odd-shaped puzzle, and so on.  The best puzzle yet was the one that had tiny comic panels as clues; I don’t remember what they ended up meaning, but I do remember being excited about it. 

     

    When I see this – something different – I love it because it means we’re in for an Aha moment.  Why are those circles there?  What do those comics mean?  The reason why I like the NYT Sunday puzzle so much is even if they don’t have some visual trick to them, discovering the common theme almost always elicits an “ah, cool.”  It’s like humor in a way – the funniest stuff is that which ‘tricks’ you; looking at something for a long time and then seeing something that wasn’t there a moment ago (and yet, was there) triggers something pleasurable in the brain.

     

    Yes, I’m going somewhere with this.  Be patient…

     

    Programming is just like crossword puzzles.  It’s a mental exercise that lets you set attainable, quantifiable goals for yourself, and receive pleasure when you achieve those goals.  And the Aha moment is the greatest part of it.  It doesn’t matter if you’re working through a new algorithm or tracking down bugs and memory leaks.  When you “get it”, you inevitably smile or let out a sigh of relief. Aha.  It gives you that little mental-adrenaline boost that keeps you coding, even if it’s well past midnight.

     

    Dan Crevier wrote a couple of blog entries (part 1, part 2) last October that talk about how to create a smoothly animating layout panel akin to what we’ve got in Project Max.  Basically, when the window is resized, his TilePanel class smoothly animates its childrens’ movement so that they layout in a grid pattern (it’s hard to describe – you can download Max to see what I’m talking about). 

     

    The blog entries are very well written, but as I looked over the code, I kept wondering how it could work if the user resized the window while objects were currently smoothly re-laying themselves out.  Offhand, it seemed like it’d require some pretty hairy tracking, but he didn’t seem to have any variables for that purpose. 

     

    It took about five minutes of thinking about the code before I got my ‘Aha’ moment; he’s found a way to let Avalon worry about all of the animation.  All he’s doing is tracking where the child object should be, and he’s letting Avalon worry about (a) where the child object currently is, and (b) how to get from there to where it should be.  This is done through the RenderTransform, which deals with the hairy tracking itself.

     

    This was a twin Aha moment – not only did I suddenly understand how his code worked, but the sheer simplicity of it provided another twinge of coooool.  He was able to offload the gross/fragile stuff to Avalon and just focus on where the child objects should be.

     

    Why mention this?  Because that particular Aha moment, like so many others, made me wonder what new doors my new found knowledge had opened.  How hard, for example, would it be to add drag/drop to his TilePanel, and get a smoothly animating TilePanel that supported child reordering?  And that question provided the impetus for me to create this blog. 

     

    So; finally, some code.

     

    The first block of posts here will focus on adding Drag/Drop.  I’ll be deriving a DragDropTilePanel from Dan’s TilePanel, with an admittedly fairly arbitrary goal of keeping the changes to the base TilePanel to a minimum. 

     

    The first (and pretty much only) change necessary in the base TilePanel class is a conceptual change.  Because his TilePanel doesn’t support adding or removing children, the ArrangeOverride method is able to just worry about “Ignoring animation, where should the child object be right now?”  He does this by using the child’s index in the list and calculating it’s grid (row/column) position.

     

    Now though, those indices can change when children are added or removed, and this requires the TilePanel to now track two things: 

     

    (1)   Where should the child object be right now, and

    (2)   Where should the child object have been the last time we moved it around?

     

    By tracking both of these variables, we can smoothly animate adding/removing child objects, and notice that we still aren’t worrying about “Where is the child object right now” – Avalon continues to do that through the child's RenderTransform.

     

    So how does the code change?  The answer is very minimally.  Here is Dan’s original ArrangeOverride, with changes marked in bold:

     

    protected override Size ArrangeOverride(Size finalSize)

    {

        // Calculate how many children fit on each row

        int childrenPerRow = Math.Max(1, (int)Math.Floor(finalSize.Width / this.ChildSize));

     

        for (int i = 0; i < this.Children.Count; i++)

        {

            FrameworkElement child = this.Children[i] as FrameworkElement;

     

            // Figure out where the child goes

            Point newOffset = CalcChildOffset(i, childrenPerRow, this.ChildSize);

     

            if (_oldChildrenPerRow != -1)

            {

                // Figure out where the child is now

     

                // If the child's index has changed, it's because something has been added or

                // removed.  We need to use the child's old index to determine its old offset.

                // The index is stored (for simplicity) in the child's DataContext.

                Point oldOffset = CalcChildOffset((int)child.DataContext, _oldChildrenPerRow, _oldChildSize);

                if (child.RenderTransform != null)

                {

                    oldOffset = child.RenderTransform.Transform(oldOffset);

                }

     

                // Transform the child from the new location back to the old position

                TranslateTransform childTransform = new TranslateTransform();

                child.RenderTransform = childTransform;

     

                // Decay the transformation with an animation

                childTransform.BeginAnimation(TranslateTransform.XProperty, MakeAnimation(oldOffset.X - newOffset.X));

                childTransform.BeginAnimation(TranslateTransform.YProperty, MakeAnimation(oldOffset.Y - newOffset.Y));

            }

     

            // Store the child's new index in the list. This'll normally

            // be the same, unless an item has been added or removed from the list.

            child.DataContext = i;

     

            // Position the child and set its size

            child.Arrange(new Rect(newOffset, new Size(this.ChildSize, this.ChildSize)));

        }

     

        _oldChildrenPerRow = childrenPerRow;

        _oldChildSize = this.ChildSize;

        return finalSize;

    }

     

    Ignoring comments, we only changed a few lines of code and we’ve now got the ability to smoothly add and remove child objects.  This works by tracking the child object’s previous index (stored for convenience in child.DataContext), which gives us where the child should have been, and the child object’s current index (which is obtained simply by enumerating the children), which gives us where the child should currently be.  Again, Avalon uses the child's RenderTransform to handle where the child currently is.  After we’re done setting the child’s position, we store the child’s new index back in child.DataContext.

     

    Storing the “previous index” in DataContext isn’t a very real-world thing to do – normally the children are objects which use DataContext for their own purposes.  I’m using it here because I want to minimize changes to the base TilePanel code - plus, it’s kind of cool to see that this all works with basic Avalon elements, not just custom “drag-enabled” objects.  Heck, try replacing one of the <Border>s with:

     

         <TextBox Text="Test" />

     

    And it’ll smoothly relayout just like any other child. Pretty cool!

     

    In order to test this, we can easily add the ability to remove a child object by clicking on it.  Add the following to the TilePanel class…

     

    protected override void OnInitialized(EventArgs e)

    {

        // If the user clicks on a child object, we want to remove it.

        foreach (FrameworkElement child in Children)

        {

            child.MouseLeftButtonDown += new MouseButtonEventHandler(ChildPressed);

        }

    }

    void ChildPressed(object sender, MouseButtonEventArgs eventArgs)

    {

        Children.Remove((UIElement)sender);

    }

     

    …and you can now click on child objects to remove them .  Try changing the animation time (the “500” in MakeAnimation) to 2000 or so, and try mixing removing child objects and resizing the border – it all works.

     

    (Note that, in what may be a bug in Avalon, you won’t get subsequent mouse down events if you click the mouse button on an object that Avalon has moved “under” the cursor – you’ll need to move the mouse to get the event).

     

    Alright, the babble/code ratio was dangerously off-kilter in this post – that shouldn’t be the norm moving forward.  Next up, dragging and dropping!

     

    I’ve attached a link to the project file.  Note that this works with the Dec CTP.  YMMV with other versions.

     

    </Jeff>

     

  • Adding to the Internet Flotsam (or is it Jetsam?)

    Greetings, program!  My wife can't stand to hear me babble on about programming, so I'm going leverage the internets (and maybe even the interwebs) to talk about code.  You may not be able to stand it either, but then, you can't make me sleep on the couch, so tough!

    I've been working for Microsoft for a while now.  When I say "a while", I mean: long enough that I can't even find an english web page that refers to the first product I worked on.  Yes, you crazy kids and your C language and your OOPses. In my day, code started on column seven, and by God not one space more!  

    I've worked on a bunch of other stuff too, some pretty cool, some very cool.  I'm currently working on UI for Project Max along with some other wise guys whose blogs you may already be reading. I'm neck-deep in Avalon (AKA WinFX for you n00bs) goodness, so I expect most of my posts will focus on that. 

    Finally, a request: if you read something here and think, "wait, there's a better way..." or "what is this guy smokin'?!?" then by all means post a comment and correct-away! 

    </Jeff>


© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker