locked
How to drag drop Compartment shape ordering RRS feed

  • Question

  • I have a compartment shape with has some order sensitive items. At the moment I handle the order of these items using an order property and sorting the collection in the DomainClass if the order property changes (with a rule).

    I'd like to improve the user experience and give drag & drop reordering of the items, but I can't see a likely extension point for this.

    I've tried double deriving the CompartmentShape and overriding the OnDrag* methods. They're only called if the header is dragged though.

    Could someone please point me in the right direction?

    Thanks

     

    Thursday, January 13, 2011 10:29 AM

Answers

  • using Microsoft.VisualStudio.Modeling;
    using Microsoft.VisualStudio.Modeling.Design;
    using Microsoft.VisualStudio.Modeling.Diagrams;
    using System.Collections.Generic;
    using System.Linq;
    
    
    // This sample allows users to re-order items in a compartment shape by dragging.
    
    
    // This example is built on the "Class Diagrams" solution template of VMSDK (DSL Tools).
    // You will need to change the following domain class names to your own:
    // ClassShape = a compartment shape
    // ClassModelElement = the domain class displayed using a ClassShape
    // This code assumes that the embedding relationships displayed in the compartments
    
    // don't use inheritance (don't have base or derived domain relationships).
    
    namespace Company.CompartmentDrag
    {
     /// <summary>
     /// Manage the mouse while dragging a compartment item.
     /// </summary>
     public class CompartmentDragMouseAction : MouseAction
     {
      private ModelElement sourceChild;
      private ClassShape sourceShape;
      private RectangleD sourceCompartmentBounds;
    
      public CompartmentDragMouseAction(ModelElement sourceChildElement, ClassShape sourceParentShape, RectangleD bounds)
       : base (sourceParentShape.Diagram)
      {
       sourceChild = sourceChildElement;
       sourceShape = sourceParentShape;
       sourceCompartmentBounds = bounds; // For cursor.
      }
       
      /// <summary>
      /// Call back to the source shape to drop the dragged item.
      /// </summary>
      /// <param name="e"></param>
      protected override void OnMouseUp(DiagramMouseEventArgs e)
      {
       base.OnMouseUp(e);
       sourceShape.DoMouseUp(sourceChild, e);
       this.Cancel(e.DiagramClientView);
       e.Handled = true;
      }
    
      /// <summary>
      /// Ideally, this shouldn't happen. This action should only be active
      /// while the mouse is still pressed. However, it can happen if you
      /// move the mouse rapidly out of the source shape, let go, and then 
      /// click somewhere else in the source shape. Yuk.
      /// </summary>
      /// <param name="e"></param>
      protected override void OnMouseDown(DiagramMouseEventArgs e)
      {
       base.OnMouseDown(e);
       this.Cancel(e.DiagramClientView);
       e.Handled = false;
      }
    
      /// <summary>
      /// Display an appropriate cursor while the drag is in progress:
      /// Up-down arrow if we are inside the original compartment.
      /// No entry if we are elsewhere.
      /// </summary>
      /// <param name="currentCursor"></param>
      /// <param name="diagramClientView"></param>
      /// <param name="mousePosition"></param>
      /// <returns></returns>
      public override System.Windows.Forms.Cursor GetCursor(System.Windows.Forms.Cursor currentCursor, DiagramClientView diagramClientView, PointD mousePosition)
      {
       // If the cursor is inside the original compartment, show up-down cursor.
       return sourceCompartmentBounds.Contains(mousePosition) 
        ? System.Windows.Forms.Cursors.SizeNS // Up-down arrow.
        : System.Windows.Forms.Cursors.No;
      }
     }
    
     /// <summary>
     /// Override some methods of the compartment shape.
     /// *** GenerateDoubleDerived must be set for this shape in DslDefinition.dsl. ****
     /// </summary>
     public partial class ClassShape
     {
      /// <summary>
      /// Model element that is being dragged.
      /// </summary>
      private static ClassModelElement dragStartElement = null;
      /// <summary>
      /// Absolute bounds of the compartment, used to set the cursor.
      /// </summary>
      private static RectangleD compartmentBounds;
    
      /// <summary>
      /// Attach mouse listeners to the compartments for the shape.
      /// This is called once per compartment shape.
      /// The base method creates the compartments for this shape.
      /// </summary>
      public override void EnsureCompartments()
      {
       base.EnsureCompartments();
       foreach (Compartment compartment in this.NestedChildShapes.OfType<Compartment>())
       {
        compartment.MouseDown += new DiagramMouseEventHandler(compartment_MouseDown);
        compartment.MouseUp += new DiagramMouseEventHandler(compartment_MouseUp);
        compartment.MouseMove += new DiagramMouseEventHandler(compartment_MouseMove);
       }
      }
    
    
      /// <summary>
      /// Remember which item the mouse was dragged from.
      /// We don't create an Action immediately, as this would inhibit the
      /// inline text editing feature. Instead, we just remember the details
      /// and will create an Action when/if the mouse moves off this list item.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      void compartment_MouseDown(object sender, DiagramMouseEventArgs e)
      {
       dragStartElement = e.HitDiagramItem.RepresentedElements.OfType<ClassModelElement>().FirstOrDefault();
       compartmentBounds = e.HitDiagramItem.Shape.AbsoluteBoundingBox;
      }
    
      /// <summary>
      /// When the mouse moves away from the initial list item, but still inside the compartment,
      /// create an Action to supervise the cursor and handle subsequent mouse events.
      /// Transfer the details of the initial mouse position to the Action.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      void compartment_MouseMove(object sender, DiagramMouseEventArgs e)
      {
       if (dragStartElement != null)
       {
        if (dragStartElement != e.HitDiagramItem.RepresentedElements.OfType<ClassModelElement>().FirstOrDefault())
        {
         e.DiagramClientView.ActiveMouseAction = new CompartmentDragMouseAction(dragStartElement, this, compartmentBounds);
         dragStartElement = null;
        }
       }
      }
    
      /// <summary>
      /// User has released the mouse button. 
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      void compartment_MouseUp(object sender, DiagramMouseEventArgs e)
      {
        dragStartElement = null;
      }
    
      /// <summary>
      /// Forget the source item if mouse up occurs outside the
      /// compartment.
      /// </summary>
      /// <param name="e"></param>
      public override void OnMouseUp(DiagramMouseEventArgs e)
      {
       base.OnMouseUp(e);
       dragStartElement = null;
      }
    
    
      /// <summary>
      /// Called by the Action when the user releases the mouse.
      /// If we are still on the same compartment but in a different list item,
      /// move the starting item to the position of the current one.
      /// </summary>
      /// <param name="dragFrom"></param>
      /// <param name="e"></param>
      public void DoMouseUp(ModelElement dragFrom, DiagramMouseEventArgs e)
      {
       // Original or "from" item:
       ClassModelElement dragFromElement = dragFrom as ClassModelElement;
       // Current or "to" item:
       ClassModelElement dragToElement = e.HitDiagramItem.RepresentedElements.OfType<ClassModelElement>().FirstOrDefault();
       if (dragFromElement != null && dragToElement != null)
       {
        // Find the common parent model element, and the relationship links:
        ElementLink parentToLink = GetEmbeddingLink(dragToElement);
        ElementLink parentFromLink = GetEmbeddingLink(dragFromElement);
        if (parentToLink != parentFromLink && parentFromLink != null && parentToLink != null)
        {
         // Get the static relationship and role (= end of relationship):
         DomainRelationshipInfo relationshipFrom = parentFromLink.GetDomainRelationship();
         DomainRoleInfo parentFromRole = relationshipFrom.DomainRoles[0];
         // Get the node in which the element is embedded, usually the element displayed in the shape:
         ModelElement parentFrom = parentFromLink.LinkedElements[0];
    
         // Same again for the target:
         DomainRelationshipInfo relationshipTo = parentToLink.GetDomainRelationship();
         DomainRoleInfo parentToRole = relationshipTo.DomainRoles[0];
         ModelElement parentTo = parentToLink.LinkedElements[0];
    
         // Mouse went down and up in same parent and same compartment:
         if (parentTo == parentFrom && relationshipTo == relationshipFrom)
         {
          // Find index of target position:
          int newIndex = 0;
          var elementLinks = parentToRole.GetElementLinks(parentTo);
          foreach (ElementLink link in elementLinks)
          {
           if (link == parentToLink) { break; }
           newIndex++;
          }
    
          if (newIndex < elementLinks.Count)
          {
           using (Transaction t = parentFrom.Store.TransactionManager.BeginTransaction("Move list item"))
           {
            parentFromLink.MoveToIndex(parentFromRole, newIndex);
            t.Commit();
           }
          }
         }
        }
       }
      }
    
      /// <summary>
      /// Get the embedding link to this element.
      /// Assumes there is no inheritance between embedding relationships.
      /// (If there is, you need to make sure you've got the relationship
      /// that is represented in the shape compartment.)
      /// </summary>
      /// <param name="child"></param>
      /// <returns></returns>
      ElementLink GetEmbeddingLink(ClassModelElement child)
      {
       foreach (DomainRoleInfo role in child.GetDomainClass().AllEmbeddedByDomainRoles)
       {
        foreach (ElementLink link in role.OppositeDomainRole.GetElementLinks(child))
        {
         // Just the assume the first embedding link is the only one.
         // Not a valid assumption if one relationship is derived from another.
         return link;
        }
       }
       return null;
      }
     }
    }
    

    - Alan -MSFT
    Tuesday, January 25, 2011 3:03 AM

All replies

  • You might get some inspiration from the techniques described here:

    In a CompartmentShape, NestedChildShapes contains among other things, one or more ElementListCompartment, which in turn contains a set of TextFields, one for each item.

    Hope that helps.


    - Alan -MSFT
    Thursday, January 13, 2011 9:56 PM
  • Did you have any luck trying to program this? I am also interested in this functionality, but while looking in the documentation I don't think that it is possible using drag and drop. So I am know thinking in the direction of customizing the right mouse click menu to change the order.
    Saturday, January 22, 2011 7:06 AM
  • Hi Pieter,

    I haven't tried this yet, but I'm also considering a right click menu item. Please, let me know if you go this way.

    Monday, January 24, 2011 1:24 PM
  • using Microsoft.VisualStudio.Modeling;
    using Microsoft.VisualStudio.Modeling.Design;
    using Microsoft.VisualStudio.Modeling.Diagrams;
    using System.Collections.Generic;
    using System.Linq;
    
    
    // This sample allows users to re-order items in a compartment shape by dragging.
    
    
    // This example is built on the "Class Diagrams" solution template of VMSDK (DSL Tools).
    // You will need to change the following domain class names to your own:
    // ClassShape = a compartment shape
    // ClassModelElement = the domain class displayed using a ClassShape
    // This code assumes that the embedding relationships displayed in the compartments
    
    // don't use inheritance (don't have base or derived domain relationships).
    
    namespace Company.CompartmentDrag
    {
     /// <summary>
     /// Manage the mouse while dragging a compartment item.
     /// </summary>
     public class CompartmentDragMouseAction : MouseAction
     {
      private ModelElement sourceChild;
      private ClassShape sourceShape;
      private RectangleD sourceCompartmentBounds;
    
      public CompartmentDragMouseAction(ModelElement sourceChildElement, ClassShape sourceParentShape, RectangleD bounds)
       : base (sourceParentShape.Diagram)
      {
       sourceChild = sourceChildElement;
       sourceShape = sourceParentShape;
       sourceCompartmentBounds = bounds; // For cursor.
      }
       
      /// <summary>
      /// Call back to the source shape to drop the dragged item.
      /// </summary>
      /// <param name="e"></param>
      protected override void OnMouseUp(DiagramMouseEventArgs e)
      {
       base.OnMouseUp(e);
       sourceShape.DoMouseUp(sourceChild, e);
       this.Cancel(e.DiagramClientView);
       e.Handled = true;
      }
    
      /// <summary>
      /// Ideally, this shouldn't happen. This action should only be active
      /// while the mouse is still pressed. However, it can happen if you
      /// move the mouse rapidly out of the source shape, let go, and then 
      /// click somewhere else in the source shape. Yuk.
      /// </summary>
      /// <param name="e"></param>
      protected override void OnMouseDown(DiagramMouseEventArgs e)
      {
       base.OnMouseDown(e);
       this.Cancel(e.DiagramClientView);
       e.Handled = false;
      }
    
      /// <summary>
      /// Display an appropriate cursor while the drag is in progress:
      /// Up-down arrow if we are inside the original compartment.
      /// No entry if we are elsewhere.
      /// </summary>
      /// <param name="currentCursor"></param>
      /// <param name="diagramClientView"></param>
      /// <param name="mousePosition"></param>
      /// <returns></returns>
      public override System.Windows.Forms.Cursor GetCursor(System.Windows.Forms.Cursor currentCursor, DiagramClientView diagramClientView, PointD mousePosition)
      {
       // If the cursor is inside the original compartment, show up-down cursor.
       return sourceCompartmentBounds.Contains(mousePosition) 
        ? System.Windows.Forms.Cursors.SizeNS // Up-down arrow.
        : System.Windows.Forms.Cursors.No;
      }
     }
    
     /// <summary>
     /// Override some methods of the compartment shape.
     /// *** GenerateDoubleDerived must be set for this shape in DslDefinition.dsl. ****
     /// </summary>
     public partial class ClassShape
     {
      /// <summary>
      /// Model element that is being dragged.
      /// </summary>
      private static ClassModelElement dragStartElement = null;
      /// <summary>
      /// Absolute bounds of the compartment, used to set the cursor.
      /// </summary>
      private static RectangleD compartmentBounds;
    
      /// <summary>
      /// Attach mouse listeners to the compartments for the shape.
      /// This is called once per compartment shape.
      /// The base method creates the compartments for this shape.
      /// </summary>
      public override void EnsureCompartments()
      {
       base.EnsureCompartments();
       foreach (Compartment compartment in this.NestedChildShapes.OfType<Compartment>())
       {
        compartment.MouseDown += new DiagramMouseEventHandler(compartment_MouseDown);
        compartment.MouseUp += new DiagramMouseEventHandler(compartment_MouseUp);
        compartment.MouseMove += new DiagramMouseEventHandler(compartment_MouseMove);
       }
      }
    
    
      /// <summary>
      /// Remember which item the mouse was dragged from.
      /// We don't create an Action immediately, as this would inhibit the
      /// inline text editing feature. Instead, we just remember the details
      /// and will create an Action when/if the mouse moves off this list item.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      void compartment_MouseDown(object sender, DiagramMouseEventArgs e)
      {
       dragStartElement = e.HitDiagramItem.RepresentedElements.OfType<ClassModelElement>().FirstOrDefault();
       compartmentBounds = e.HitDiagramItem.Shape.AbsoluteBoundingBox;
      }
    
      /// <summary>
      /// When the mouse moves away from the initial list item, but still inside the compartment,
      /// create an Action to supervise the cursor and handle subsequent mouse events.
      /// Transfer the details of the initial mouse position to the Action.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      void compartment_MouseMove(object sender, DiagramMouseEventArgs e)
      {
       if (dragStartElement != null)
       {
        if (dragStartElement != e.HitDiagramItem.RepresentedElements.OfType<ClassModelElement>().FirstOrDefault())
        {
         e.DiagramClientView.ActiveMouseAction = new CompartmentDragMouseAction(dragStartElement, this, compartmentBounds);
         dragStartElement = null;
        }
       }
      }
    
      /// <summary>
      /// User has released the mouse button. 
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      void compartment_MouseUp(object sender, DiagramMouseEventArgs e)
      {
        dragStartElement = null;
      }
    
      /// <summary>
      /// Forget the source item if mouse up occurs outside the
      /// compartment.
      /// </summary>
      /// <param name="e"></param>
      public override void OnMouseUp(DiagramMouseEventArgs e)
      {
       base.OnMouseUp(e);
       dragStartElement = null;
      }
    
    
      /// <summary>
      /// Called by the Action when the user releases the mouse.
      /// If we are still on the same compartment but in a different list item,
      /// move the starting item to the position of the current one.
      /// </summary>
      /// <param name="dragFrom"></param>
      /// <param name="e"></param>
      public void DoMouseUp(ModelElement dragFrom, DiagramMouseEventArgs e)
      {
       // Original or "from" item:
       ClassModelElement dragFromElement = dragFrom as ClassModelElement;
       // Current or "to" item:
       ClassModelElement dragToElement = e.HitDiagramItem.RepresentedElements.OfType<ClassModelElement>().FirstOrDefault();
       if (dragFromElement != null && dragToElement != null)
       {
        // Find the common parent model element, and the relationship links:
        ElementLink parentToLink = GetEmbeddingLink(dragToElement);
        ElementLink parentFromLink = GetEmbeddingLink(dragFromElement);
        if (parentToLink != parentFromLink && parentFromLink != null && parentToLink != null)
        {
         // Get the static relationship and role (= end of relationship):
         DomainRelationshipInfo relationshipFrom = parentFromLink.GetDomainRelationship();
         DomainRoleInfo parentFromRole = relationshipFrom.DomainRoles[0];
         // Get the node in which the element is embedded, usually the element displayed in the shape:
         ModelElement parentFrom = parentFromLink.LinkedElements[0];
    
         // Same again for the target:
         DomainRelationshipInfo relationshipTo = parentToLink.GetDomainRelationship();
         DomainRoleInfo parentToRole = relationshipTo.DomainRoles[0];
         ModelElement parentTo = parentToLink.LinkedElements[0];
    
         // Mouse went down and up in same parent and same compartment:
         if (parentTo == parentFrom && relationshipTo == relationshipFrom)
         {
          // Find index of target position:
          int newIndex = 0;
          var elementLinks = parentToRole.GetElementLinks(parentTo);
          foreach (ElementLink link in elementLinks)
          {
           if (link == parentToLink) { break; }
           newIndex++;
          }
    
          if (newIndex < elementLinks.Count)
          {
           using (Transaction t = parentFrom.Store.TransactionManager.BeginTransaction("Move list item"))
           {
            parentFromLink.MoveToIndex(parentFromRole, newIndex);
            t.Commit();
           }
          }
         }
        }
       }
      }
    
      /// <summary>
      /// Get the embedding link to this element.
      /// Assumes there is no inheritance between embedding relationships.
      /// (If there is, you need to make sure you've got the relationship
      /// that is represented in the shape compartment.)
      /// </summary>
      /// <param name="child"></param>
      /// <returns></returns>
      ElementLink GetEmbeddingLink(ClassModelElement child)
      {
       foreach (DomainRoleInfo role in child.GetDomainClass().AllEmbeddedByDomainRoles)
       {
        foreach (ElementLink link in role.OppositeDomainRole.GetElementLinks(child))
        {
         // Just the assume the first embedding link is the only one.
         // Not a valid assumption if one relationship is derived from another.
         return link;
        }
       }
       return null;
      }
     }
    }
    

    - Alan -MSFT
    Tuesday, January 25, 2011 3:03 AM
  • Alan, I owe you a beverage! :) Is that a sample you've just knocked up, or was it floating around and I completely missed it?

     

    Thanks!

    Tuesday, January 25, 2011 9:05 AM
  • Ok this code does indeed work. Tnx for the support!
    Tuesday, January 25, 2011 10:26 AM
  • Just my evening's entertainment. :)
    - Alan -MSFT
    Tuesday, January 25, 2011 10:30 AM
  • Thanks again, it works great for a compartment populated from a standard embedding relationship (as your comments claim). 

    While I've got your attention :)

    My model is base on the StateMachine lab. Transition ordering is important though. I'm trying to reorder transitions between states. I'm doing that by having GetEmbeddingLink just return the child if it's an ElementLink. This style of code works fine under test (i.e. My State.Successors property returns my newly reordered list). But when I do it with the diagram I'm hitting some issues under CompartmentItemRolePlayerPositionChangeRule.RolePlayerPositionChanged.

    Particularly, it's attempting to cast a State to a Transition on a call to GetStateForStateShapeTransitions, and failing.

    I've changed the generate code to the following, and now it works. But it doesn't seem like the appropriate place to be doing this (for one, I'll lose it as soon as I regenerate!), and two, this rule is a sealed method, so I can't tack this on through extension. I assume I'm either doing something wrong in my reorder code.

    	if
    (typeof
    (
    Transition
    ).IsAssignableFrom(e.DomainRelationship.ImplementationClass))
    {
    if (!e.CounterpartDomainRole.IsSource)
    {
    global ::System.Collections.IEnumerable  elements ;
                            State  state = e.CounterpartRolePlayer as  State ;

                            if  (state == null )
                                elements = CompartmentItemAddRule .GetStateForStateShapeTransitions((Transition )e.CounterpartRolePlayer);
                            else
                                elements = state.Successors;

    CompartmentItemAddRule .UpdateCompartments(elements, typeof (StateShape ), "Transitions" , repaintOnly);
    }
    }
    Tuesday, January 25, 2011 12:34 PM
  • Sorry, I can't quite understand that. Are you displaying the transitions both in a compartment and as connectors? That should work OK.
    If you're willing to share your code, I could have a look. alan.wills at microsoft dot com
    - Alan -MSFT
    Tuesday, January 25, 2011 2:54 PM
  • Ahh, the formatting probably doesn't help.:-)

    Yes. Transitions are a zeromany Reference relationship between states with predecessor on one side and successor on the other. I'm showing the successor transitions in a compartment on a state. I'm also mapping the relationship to a connector.

    We care about order because of the order of evaluation of conditions on the transition.

    The code I pasted does solve the invalid cast exception problem, but unfortunately it's in a generated file - Diagram.cs. And a sealed class and method at that. :-( If I just stick with the code that's generated then there is a line

     

    elements = CompartmentItemAddRule
    .GetStateForStateShapeTransitions((Transition
    )e.CounterpartRolePlayer);
    
    that causes an invalid cast exception. I think it's supposed to be guarded by the line
    if
    (typeof
    (Transition
    ).IsAssignableFrom(e.DomainRelationship.ImplementationClass))
    

    but in the case of reordering the transitions the ImplementationClass is not the same type as the CounterpartRolePlayer.

    I'll have to check re sharing code. :-)

    Tuesday, January 25, 2011 6:07 PM
  • OK, just to check my understanding:

    • You're displaying the domain relationship "Transition" as a compartment list item.
    • You've added the re-ordering code above, adapting it to work on the relationship.
    • In Diagram.cs, CompartmentItemRolePlayerPositionChangeRule throws an invalid cast when, as a user, you try to re-order an item.

    It looks as though there's a bug in the DSL framework that appears when you try to display a link in a compartment. In fact, there's also a bug that results in incorrect Undo after you delete a link: After Undo, the link doesn't reappear in the compartment.

    Workaround: It might be easier to display the target state elements in the definition rather than the transitions themselves. (Unless you allow more than one transition between any two states?)

    In the DSLDefinition, click the shape map between your domain class "State" and the compartment shape "StateShape"; then in the DSL Details window, click Compartment Maps; then click the compartment; and look at "Displayed elements collection path". I'm guessing that in there, you've got something like

         StateReferencesTargets.Targets

    Instead, I'd recommend you navigate one step further, so that it's something like:

         StateReferencesTargets.Targets/!Target

    Then the list will display the name of the target state instead of the relationship. That will work better.

     


    - Alan -MSFT
    Wednesday, January 26, 2011 2:49 PM
  • Thanks for your help Alan, I'd tried that as well, but it turns out I do have a requirement for multiple transitions between states.

     

    Friday, January 28, 2011 2:52 PM
  • Well, this is a sort of last resort thing, but you could just remove the .tt file and leave your edited .cs file.

    Or if you want, you could fix the DSL include file that generates the code, and adjust your .tt file to point to your fixed version. The include files are in \Program Files\Microsoft Visual Studio 10.0\Common7\IDE\Extensions\Microsoft\DSL SDK\DSL Designer\10.0\TextTemplates\Dsl

     


    - Alan -MSFT
    Friday, January 28, 2011 3:01 PM
  • Thanks again for your help Alan, I've no idea how long it would've taken to figure out the nuances of the drag drop without it!
    Friday, January 28, 2011 5:56 PM
  • That really works like a charm! Thx a lot for sharing that!

    (Just wanted to let you know that it helped out another person searching on this topic!)

    Thanks again!

    Sönke


    Monday, August 27, 2012 5:39 PM
  • This code is great, but can be improved slightly by adding the line

    e.DiagramClientView.Selection.Set(e.HitDiagramItem);

    immediately before

    t.Commit();

    This ensures the selection stays on the dragged item, which otherwise can be a bit visually confusing to the user.

    Monday, December 2, 2013 5:45 PM