September, 2007

  • Cloudy in Seattle

    Have you seen the Toolbox Controls Installer?

    • 5 Comments

    A really cool new feature for Visual Studio 2008 is the Toolbox Controls Installer which as the name implies, allows you to add controls to the toolbox.  It's much easier than some of the previous VS package type work you used to need to do.

    There is a great sample in the Visual Studio SDK 2008 that shows you how to add a Windows Forms control, Web Control and WPF control to the toolbox programmatically.  The sample is called "Controls" -- and includes a WPF control and design time that may be familiar to those of you who have been reading my blog. (I'm surprised at how much mileage some of my samples get!)

    There is one caveat, in some of the online documentation that doesn't show the WPF example (it was added later), there is a missing registry value that is required to make this work:

                <Registry Root="HKLM" Key="$(var.VisualStudioRegistryRoot)\ToolboxControlsInstaller\CustomControlLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1631b3e7a8fbcce5" Value="Litware Controls" Type="string">
                  <Registry Name="Codebase" Value="[#CustomControlLibrary.dll]" Type="string" />
                  <Registry Name="WPFControls" Value="1" Type="string" />
                </Registry>

    The "WPFControls" value above.

    Very cool feature, really simplifies toolbox installation.  Note: this is implemented in VS 2008 Beta 2.

  • Cloudy in Seattle

    Dealing with Application.Current in Design Mode

    • 3 Comments

    There are a ton of WPF samples on MSDN which are really useful if you are developing in or learning WPF.  If you aren't aware of these samples, please check them out.

    One of the samples illustrates a point I want to make in my post today -- which is that as much as we try to avoid making you do this, sometimes you have to tweak your XAML/code in order to make it work within the Cider designer.

    The sample I am going to use is from a DataTemplate Sample located here:  http://msdn2.microsoft.com/en-us/library/aa972119.aspx

    If you load up this sample in VS 2008 beta 2 or later and (after conversion) you'll see that you can build and run the application no problem.  However, if you open up Window1.xaml, you'll notice that it fails to load in the designer and you'll get an error:

     'myTaskTemplate' resource not found.
       at MS.Internal.Helper.FindResourceHelper.DoTryCatchWhen(Object arg)
       at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Boolean isSingleParameter)
       at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler) 

    The problem occurs when we try to instantiate the TaskListDataTemplateSelector which is in the Window.Resources:

        <local:TaskListDataTemplateSelector x:Key="myDataTemplateSelector"/>

    This class does the following:

        public class TaskListDataTemplateSelector : DataTemplateSelector
        {
            public override DataTemplate
                SelectTemplate(object item, DependencyObject container)
            {
                if (item != null && item is Task)
                {
                    Task taskitem = item as Task;
                    Window window = Application.Current.MainWindow;

                    if (taskitem.Priority == 1)
                        return
                            window.FindResource("importantTaskTemplate") as DataTemplate;
                    else
                        return
                            window.FindResource("myTaskTemplate") as DataTemplate;
                }

                return null;
            }
        }

    Do you see the problem? What do you suppose Application.Current.MainWindow is in the designer?

    Since Visual Studio isn't a WPF application, for a long time during the development of Cider Application.Current returned null.  It no longer returns null but it is setup by Cider and has to do with Cider and not what you will get when you run your application. 

    In other words, calls to window.FindResource() will fail.

    So what can you do to make this work in the designer?  Well you have a few options, but if you've been following this blog for a long time you'll remember this post on detecting design mode.

    Since this situation nicely demonstrates the use of design mode, I'll solve the problem that way by changing the above code to check for Design Mode before accessing Application.Current.

            public override DataTemplate
                SelectTemplate(object item, DependencyObject container)
            {
                if (!DesignerProperties.GetIsInDesignMode(container) && item != null && item is Task)
                {
                    Task taskitem = item as Task;
                    Window window = Application.Current.MainWindow;

               (. . .)
            }

    The parameter you pass to GetIsInDesignMode doesn't really matter here since I'm counting on the default value which has been overriden to true in the context of a designer, the DependencyObject passed in will look for the IsInDesignMode attached property up it's tree.

    Now -- lo and behold!  The designer loads in Cider.

  • Cloudy in Seattle

    Design Time Adorners

    • 2 Comments

    I've posted about design time adorners a few times in the past however there have been some API changes since then so I thought I'd do a fresh post.

    What are design time adorners?

    Adorners are WPF elements that you can put on the Cider design surface when your custom control is instantiated and selected.  The end user who is using Cider can interact with your adorners to make updates to the XAML.

    For example, the selection handles, grid lines and rails when a Grid is selected are put on the design surface through the same adorner infrastructure I am describing in this post.

    Create an AdornerProvider

    The first step is to create an AdornerProvider that will add your adorners when your control is selected.  You will associate this AdornerProvider to your control using the Metadata Store as described in this post and this post.

    public class MyCustomAdornerProvider : PrimarySelectionAdornerProvider {
    Public MyCustomAdornerProvider () {
        Button buttonAdorner = new Button();
        buttonAdorner.Background = Brushes.Red;
        AdornerPanel panel = new AdornerPanel();
        panel.Children.Add(buttonAdorner);
        Adorners.Add(panel);
    } (...) }

    The key things to point out here are:

    • MyCustomAdornerProvider derives from PrimarySelectionAdornderProvider.  This means the adorners will light up when the control MyCustomAdornerProvder is applied to is selected.
    • Adorners are added to the surface by adding them to the Adorners property from the base class
    • An AdornerPanel is provides a lot of size/positioning help.  You don't strictly need it, an adorner added directly to the Adorners collection will show up in the upper left hand corner of the control it is associated with. 

    Add UIElements (the Adorners) to an AdornerPanel

    Setup the AdornerPlacementCollection with Position and Size Methods that are additive (take a factor + offset):

    • PositionRelativeToAdorner[Height/Width]
    • PositionRelationToContent[Height/Width]
    • SizeRelativeToAdornerDesired[Height/Width]
    • SizeRelativeToContent[Height/Width]

    In this API, "Content" refers to the control being adorned as its layed out on the design surface at 1X zoom.

    For example, to setup a button that will be above and to the left of the associated control when it is selected, you would do the following: 

    AdornerPlacementCollection placement = new AdornerPlacementCollection();
    placement.SizeRelativeToAdornerDesiredHeight(1.0, 0);
    placement.SizeRelativeToAdornerDesiredWidth(1.0, 0);
    placement.PositionRelativeToAdornerHeight(-1, -NudgeAmount);
    placement.PositionRelativeToAdornerWidth(-1, -NudgeAmount);
    AdornerPanel.SetPlacements(adorner, placement);

    To control how your adorners will act when zooming in and out, you can set the stretch properties on the AdornerPanel:

    AdornerPanel.Set[Vertical/Horizontal]Stretch

    Handle the interaction with your Adorners (handle events or setup tasks)

    For a lot of adorners that you will put on the design surface, handling the interaction is as simple as handling the events on the adorner itself.  For example:

    BouncyButton adorner = new BouncyButton();
    adorner.Click += new RoutedEventHandler(OnButtonClick);

    And in OnButtonClick() you can make changes to the XAML by working through the Editing Object Model.  The following sample code shows how to get the ModelItem for a given adorner and modify one of its properties through the ModelProperty class.

    ModelItem adornedElementModelItem = AdornerProperties.GetModel(_ColorsListControl);
    adornedElementModelItem.Properties[Control.BackgroundProperty].SetValue(new SolidColorBrush(_ColorsListControl.SelectedColor));

    Hook up your AdornerProvider to the control via the FeatureAttribute and MetadataStore

    The other thing you can do is use Tasks.  A Task associates a collection of input gestures that are bound to commands and the handlers for those commands.  Cider defines a set of ToolGestures that you can use as input gestures and thus handle -- in other words you can handle a click gesture on a Rectangle even though the Rectangle doesn't have a Click event.

    Task clickTask = new Task();
    clickTask.ToolCommandBindings.Add(new ToolCommandBinding(_ClickCommand, OnRectangleClick));
    clickTask.InputBindings.Add(new InputBinding(_ClickCommand, new ToolGesture(ToolAction.Click)));
    AdornerPanel.SetTask(adorner, clickTask);

    And as before, in OnRectangleClick() you can use the Editing Model (ModelItem, ModelProperty) to update the XAML from your adorner.

    Setup the Metadata

    Finally, in your Metadata Assembly, you will set up the metadata to associate your AdornerProvider to the control you want it to show up for:

    builder.AddCustomAttributes(typeof(ButtonWithDesignTime), new FeatureAttribute(typeof(PopupButtonAdornerProvider)));

    Finally

    I've attached a sample to this blog post that shows all of the concepts above and will work on beta 2 and RTM.

  • Cloudy in Seattle

    Using a Design Time Adorner to Setup Bindings Between Controls

    • 3 Comments

    [One warning -- the code to set the binding via the Editing Object Model will not work in VS 2008 beta 2 but will work in RTM] 

    Recently I wrote a post about writing Design Time Adorners. I had a good question come up on our Cider forum and I thought I would answer that question in the form of a post and sample which will illustrate how to accomplish much of what the person who made the post was trying to do.

    The question was essentially: How can I use design time adorners to make it easy to setup bindings between controls?

    I've updated my sample from my post on writing Design Time Adorners to not only set the Background of my SimpleCustomControl but also to set the Background of all of the siblings of my control to bind to the Background of my control -- as long as my SimpleCustomControl has a name and the Background property of the sibling control is not set.

    I piggy backed on the adorner that is already there so this post is really about using the Editing Object Model than adorners per se as the Editing Object Model is used in many situations, not just with adorners.

    How the design time works

    1) I start with my SimpleCustomControl in a Grid with 3 Buttons and 1 label as siblings.  Notice that one of the Buttons has it's Background set.

    2) I click on the Adorner for the SimpleCustomControl (the paint can that appears when the SimpleCustomControl is selected) and select a color.

    3) Notice how the Background for the siblings that didn't have the Background property set is now set to a binding to the Background of my SimpleCustomControl.

    So how did I get this to work?

    Implementation

    As mentioned above, I added all of the functionality to the existing handler for when the adorner gets clicked -- inside of _ColorsListControl_ClosePopup():

    void _ColorsListControl_ClosePopup(object sender, RoutedEventArgs e)
    {
         (. . .)
    }

    The first step is to get at the ModelItems for the siblings of the SimpleCustomControl.  I get at them by going to the parent of the SimpleCustomControl (the Grid) and then asking for all of the Grids children. 

                ModelItem parentModelItem = adornedElementModelItem.Parent;
                ModelProperty siblingsProperty = parentModelItem.Properties["Children"];
                ModelItemCollection siblings = siblingsProperty.Collection;

    The next step is to enumerate through those ModelItems and if the Background property is not set, set it to a binding:

                    foreach (ModelItem mi in siblings)
                    {
                        if (mi != adornedElementModelItem && !mi.Properties[Control.BackgroundProperty].IsSet)
                        {
                                (. . .) // set to a Binding
                        }
                    }

    Setting the Background to a Binding is done by creating a Binding instance, and setting the Background property of the sibling to it.  Then we setup the Binding by setting both the ElementName and Path properties.

                            using (var scope = mi.BeginEdit())
                            {
                                ModelItem binding = ModelFactory.CreateItem(Context, typeof(Binding));
                                mi.Properties[Control.BackgroundProperty].SetValue(binding);
                                binding.Properties["ElementName"].SetValue(adornedElementModelItem.Properties["Name"].Value);
                                binding.Properties["Path"].SetValue(new PropertyPath("Background"));

                                scope.Complete();
                            }

    And that's it -- one caveat is that as all my posts, this is quick and dirty code to illustrate a concept and this code may have to be tweaked to handle all of the various situations that can arise in the usage of the SimpleCustomControl.

    The code for this sample is attached to this blog post.

Page 1 of 1 (4 items)