WPF & Silverlight Design-Time Code Sharing – Part I

This post along with Part II will cover writing design-time code that can be shared between WPF & Silverlight custom controls for the WPF & Silverlight Designer for Visual Studio 2010 that target .NET 4.0 and Silverlight 3 and 4.

Normally in a series you start with a simple example and article.  For this series I need to start with a level 300-400 example because this article and code is for the Development Tools Ecosystem Summit presentation that Mark Boulter and I did.

This post and code is for the Development Tools Ecosystem Summit presentation that Mark Boulter and I did.  The presentation was on how to write a design-time assembly that could be shared between WPF and Silverlight custom controls.

Part I of this post covers Feedback and Rating control, design-time assembly discovery, loading and metadata creation.

Part II of this post will cover the control design-time code.

Table of Contents

  1. Tutorial Breadcrumb Trail
  2. Background
  3. WPF & Silverlight Feedback Control
  4. Required Design-Time Features
  5. Design-Time Assembly Locating
  6. WPF Metadata Loading
  7. Silverlight Metadata Loading
  8. Metadata Building
  9. Downloads
  10. Online Documentation Status
  11. More Information Links
  12. Comments

Tutorial Breadcrumb Trail 

When you open the source code solution, be sure to open your Task List window so that you can view and walk the step by step tutorial breadcrumb trail I’ve added to the solution.  When you click on each TODO you’ll be taken to that section of code.

BreadcrumbTrail

Background 

For the purpose of this article and the code, our fictitious company name is, Cider Controls.  The first control we have ready for release is our WPF & Silverlight Feedback control.  The Feedback control contains our Rating control that allows a value to be selected at run-time using the mouse.  Our assignment is to write a design-time for Visual Studio 2010 for this control.

Note: Setting the rating value at design-time is probably something the control does not actually need.  However, it serves its purpose by illustrating how to use the extensibility features of the Designer.

WPF & Silverlight Feedback Control 

FeedbackControl FeedbackControlClassDiagram

The blue text is the Header property.  This property is of type Object.  Developers can supply simple text or use nested XAML to create any type of Header they want.

The text under the Header is the CommentHeading property which is a String.

The TextBox is bound to the Comment property which is a String.

The . . . . . is the Rating control.  This is bound to the Value property which is an Integer.

The CornerRadius property binds to the Border control in the control template.

The Submit Button is enabled when the Value is greater than zero and the Comment is not empty or null.

Required Design-Time Features 

  • Support both the WPF & Silverlight versions of the Feedback control
  • Set control value at design-time using a context menu
  • Set control value at design-time using the Rating control in an adorner
  • Set control value at design-time using the Rating control is the properties window
  • Set all control properties in the properties window
  • Provide a Category Editor for the properties window that allows editing the Feedback control properties assigned to the Custom category

The Feedback Value property is zero.

MenuAction

Adorner

After the Feedback Value property has been set, both the control and adorner display the value.

Adorner

Inline Value Editor

Inline Value editor uses the Rating control just like the Feedback control and adorner.

ValueEditor

Category Editor

CategoryView

Design-time XAML

Notice the Feedback Header that takes XAML.  You could easily add a Grid or StackPanel and then display and image and text if you wanted.

 <cc:Feedback 
    HorizontalAlignment="Left" Grid.Column="1" 
    VerticalAlignment="Top" Height="176" Width="304" 
    CommentHeading="Write a design time for your control today."
    CornerRadius="10"
    Padding="3"
    BorderBrush="DarkGray"
    BorderThickness="3"
    Background="WhiteSmoke" Value="3" Margin="12,24,0,0">

    <cc:Feedback.Header>
        <TextBlock 
            Foreground="Blue" FontSize="16" FontWeight="Bold" 
            Text="Control Design Time Example" />
    </cc:Feedback.Header>

</cc:Feedback>

Design-Time Assembly Locating 

Writing a design-time for a control is actually pretty simple and the code tends to be small blocks since each individual feature you add is almost self-contained.

There are two complicated pieces, metadata loading and Toolbox installation.

Simplified:  Metadata loading is the design-time process that Visual Studio uses to discover and associate your design-time code with your control so that the Designer will run it at design-time when you want it to.

I’ll cover Toolbox installation in a future post.

These two can be complicated because there are different ways to do both, some can involve the registry, some involve multiple assemblies and both depend on a naming convention.  In other words, when you first look at this, you’ll think there are a number of moving parts, but after you’ve done this a few times, you’ll have it down.

Design-Time Load Scenarios

  • WPF Custom Control Designer - (single design assembly)
  • Silverlight Custom Control Designer - (single or two design assemblies)
  • Code Sharing for WPF & Silverlight controls (five design assemblies)

Note:  You can use other techniques for reducing the number of assemblies when doing multi-platform design-times, but they can lead to bugs and confusion due to platform mixing and won’t be discussed here.

For this post I’ll only cover the, “Code Sharing for WPF & Silverlight controls” scenario.  The techniques that I’ll describe here are used in Microsoft production code and simplify code sharing.  In future posts I’ll cover the other design-time load scenarios and techniques.

Design-Time Assembly Naming and Location

Assembly name Notes
CiderControls.WPF.dll Custom Control Run-Time Assembly
CiderControls.WPF.Design.*.dll Loaded by Visual Studio and Blend
\Design\CiderControls.WPF.Design.*.dll Located in the \Design sub folder.  Loaded by Visual Studio and Blend
CiderControls.WPF.VisualStudio.Design.*.dll Loaded only by Visual Studio
CiderControls.WPF.Expression.Design.*.dll Loaded only by Blend
\Design\CiderControls.WPF.VisualStudio.Design.*.dll Loaded only by Visual Studio
\Design\CiderControls.WPF.Expression.Design.*.dll Loaded only by by Blend

Looking closely at the above table, you’ll see a naming pattern.  The control assembly can be named whatever the control author wants.  Typically you’ll see the vendor name followed by the product and possibly version number.

The design-time assemblies must be named according to the above table in order for Visual Studio to load them.

For metadata loading Visual Studio loads the .dll’s in the above order.  Meaning .VisualStudio.Design. qualified .dll’s will load second and overwrite metadata in the .Design dll.

The “*” represents optional additional name text that the design-time assembly author can add.  Normally this is not used.  This provides a way to ship additional design assemblies that follow the above naming convention.  (See next paragraph.)

When the metadata is loaded there is one more consideration that is taken into account.  The version number of the Microsoft.Windows.Design.Extensibility.dll or Microsoft.Windows.Design.Interaction.dll that the design-time assembly is compiled against will be compared to those versions on the target machine.

For readability I’ll use MWD in place of Microsoft.Windows.Design.Extensibility.dll in the next short section.

The Design.*.dll nomenclature allows for multiple design assemblies fitting a pattern such as “foo.Design.*.dll”, one for each version of the MWD interface.  Zero or one assembly fitting a pattern will load:

  • If the MWD version referenced by the design-time assembly has a different major version number than the user’s machine MWD version, then the design-time assembly will not load and is bypassed.
  • If more than one design-time assembly is compatible with the user’s machine MWD version, the Designer loads the version compiled against the highest MWD version that is less than or equal to the user’s machine MWD version.

For instance, if the user’s machine MWD version is 4.1.3.0 and there are 4 design-time assemblies matching the pattern “foo.design*.dll”, each compiled against the below MWD versions, the designer reacts as follows:

  • 3.0.1.0: Will not load.  Incompatible API version.
  • 4.0.1.0: Would load if there were not a higher matching version, but since 4.1.1.0 exists it does not load 
  • 4.1.1.0: Version loaded as it is closest to the current version number without exceeding it
  • 4.3.0.0: Will not load in the Designer as it was built against a newer version of the MWD’s

Metadata and Design Code

The design-time assembly does not have to actually have any design time code in it.  In fact as you’ll see shortly, the two design-time assemblies that our solution has that follow the above naming convention only serve as shims between Visual Studio 2010 and our common WPF and Silverlight design-time assembly that does all the work.

WPF

The below screen shot pictures the WPF run-time custom control folder and contents.

CiderControlsFolderOne

The below screen shot pictures the contents of the \Design\ subfolder.

CiderControlsFolderTwo

In the above image, CiderControls.WPF.VisualStudio.Design.dll is the assembly that Visual Studio will discover and load the design-time metadata from.

CiderControls.Common.VisualStudio.Design.dll is in this folder because it is referenced by CiderControls.WPF.VisualStudio.Design.dll.

CiderControls.WPF.dll is in this folder because it is referenced by the common design-time assembly CiderControls.Common.VisualStudio.Design.dll.

Silverlight

The below screen shot pictures the Silverlight run-time custom control folder and contents.

CiderControlsSLFolderOne

The below screen shot pictures the contents of the \Design\ subfolder.

CiderControlsSLFolderTwo

In the above image, CiderControls.Silverlight.VisualStudio.Design.dll is the assembly that Visual Studio will discover and load the design-time metadata from.

CiderControls.Common.VisualStudio.Design.dll is in this folder because it is referenced by CiderControls.Silverlight.VisualStudio.Design.dll.

CiderControls.Silverlight.VisualStudio.Design.Types.dll is in this folder because it is referenced by CiderControls.Common.VisualStudio.Design.dll and CiderControls.Silverlight.VisualStudio.Design.dll.

The above two file references are not a project references but a file or binary references.  When this reference is added, the browse button is used to browse to the CiderControls.Silverlight.dll and the reference added to the file.

CiderControls.Silverlight.dll is in this folder because it is referenced by CiderControls.Silverlight.VisualStudio.Design.Types.dll.

CiderControls.WPF.dll is in this folder because it is referenced by the common design-time assembly CiderControls.Common.VisualStudio.Design.dll.

Note: CiderControls.Silverlight.dll and CiderControls.Silverlight.VisualStudio.Design.Types.dll are the only two assemblies that actually reference Silverlight, the remaining assemblies are all WPF assemblies.

Design Subfolder

In both of the above series of images, the \Design subfolder is under the \Bin\Debug folder for each custom control assembly.  During development, the best way to populate the \Design subfolder is to set up a Post-build event.  Now your design-time test projects only need to reference the design-time assembly and Visual Studio will automatically load your design-time assembly for you.

For the CiderControls.WPF.VisualStudio.Design.dll project I’m using the following Post-build event command line:

    xcopy "$(TargetDir)*.*"   "$(SolutionDir)\CiderControls.WPF\Bin\Debug\Design" /S /I /Y

For the CiderControls.Silverlight.VisualStudio.Design.dll project I’m using the following Post-build event command line:

    xcopy "$(TargetDir)*.*"   "$(SolutionDir)\CiderControls.Silverlight\Bin\Debug\Design" /S /I /Y

WPF Metadata Loading 

Visual Studio locating your correctly named design-time assembly is step one.  That assembly must also decorated with the ProvideMetadata attribute:

  [assembly: ProvideMetadata(typeof(RegisterMetadata))]

The ProvideMetadata constructor requires that a type be passed in.  That type must implement the IProvideAttributeTable interface.

IProvideAttributeTable interface has a single read-only property AttributeTable that returns an AttributeTable.

Below is the RegisterMetadata class that is in the CiderControls.WPF.VisualStudio.Design project.

 using CiderControls.Common.VisualStudio.Design.Registration;
using CiderControls.WPF.VisualStudio.Design;
using Microsoft.Windows.Design.Metadata;

[assembly: ProvideMetadata(typeof(RegisterMetadata))]

namespace CiderControls.WPF.VisualStudio.Design {

  internal class RegisterMetadata : IProvideAttributeTable {

    AttributeTable IProvideAttributeTable.AttributeTable {
      get {
        CiderControlsAttributeTableBuilder builder =
            new CiderControlsAttributeTableBuilder(new WPFTypeResolver());
        return builder.CreateTable();
      }
    }
  }
}

Within the AttributeTable property we instantiate the CiderControlsAttributeTableBuilder class that derives from AttributeTableBuilder.  AttributeTableBuilder provides the CreateTable method.

The CiderControlsAttributeTableBuilder is located in the “platform neutral” CiderControls.Common.VisualStudio.Design assembly.

The WPFTypeResolver is passed into the CiderControlsAttributeTableBuilder constructor.  This class allows the “platform neutral” CiderControls.Common.VisualStudio.Design assembly to resolve WPF types in our run-time control assembly by referring to them with platform neutral TypeIdentifiers.  The WPFTypeResolver is located in the CiderControls.WPF.VisualStudio.Design project.

The CiderControlsAttributeTableBuilder is the platform neutral class that does all the heavy lifting for creating the required AttributeTable; also know as our metadata.

Below is the WPFTypeResolver class that is in the CiderControls.WPF.VisualStudio.Design project.  The GetPlatformType method takes a platform neutral TypeIdentifier and returns a platform specific type.

 using System;
using CiderControls.Common.VisualStudio.Design.Infrastructure;
using CiderControls.Common.VisualStudio.Design.Registration;
using Microsoft.Windows.Design.Metadata;

namespace CiderControls.WPF.VisualStudio.Design {

  internal class WPFTypeResolver : RegistrationTypeResolver {

    public override Type GetPlatformType(TypeIdentifier id) {
      switch (id.Name) {

        case Constants.STR_CIDERCONTROLSFEEDBACK:
          return typeof(CiderControls.Feedback);

        case Constants.STR_CIDERCONTROLSRATING:
          return typeof(CiderControls.Rating);
      }

      throw new ArgumentOutOfRangeException("id.Name", id.Name, message...);
    }
  }
}

Silverlight Metadata Loading 

Visual Studio locating your correctly named design-time assembly is step one.  That assembly must also decorated with the ProvideMetadata attribute:

  [assembly: ProvideMetadata(typeof(RegisterMetadata))]

The ProvideMetadata constructor requires that a type be passed in.  That type must implement the IProvideAttributeTable interface.

IProvideAttributeTable interface has a single read-only property AttributeTable that returns an AttributeTable.

Below is the RegisterMetadata class that is in the CiderControls.Silverlight.VisualStudio.Design project.

 using CiderControls.Common.VisualStudio.Design.Registration;
using CiderControls.Silverlight.VisualStudio.Design;
using Microsoft.Windows.Design.Metadata;

[assembly: ProvideMetadata(typeof(RegisterMetadata))]

namespace CiderControls.Silverlight.VisualStudio.Design {

  internal class RegisterMetadata : IProvideAttributeTable {

    AttributeTable IProvideAttributeTable.AttributeTable {
      get {
        CiderControlsAttributeTableBuilder builder =
          new CiderControlsAttributeTableBuilder(new SilverlightTypeResolver());
        return builder.CreateTable();
      }
    }
  }
}

Within the AttributeTable property we instantiate the CiderControlsAttributeTableBuilder class that derives from AttributeTableBuilder.  AttributeTableBuilder provides the CreateTable method.

The CiderControlsAttributeTableBuilder is located in the “platform neutral” CiderControls.Common.VisualStudio.Design assembly.

The SilverlightTypeResolver is passed into the CiderControlsAttributeTableBuilder constructor.  This class allows the “platform neutral” CiderControls.Common.VisualStudio.Design assembly to resolve Silverlight types in our run-time control assembly by referring to them with platform neutral TypeIdentifiers.  The SilverlightTypeResolver is located in the CiderControls.Silverlight.VisualStudio.Design project.

The CiderControlsAttributeTableBuilder is the platform neutral class that does all the heavy lifting for creating the required AttributeTable; also know as our metadata.

Below is the SilverlightTypeResolver class that is in the CiderControls.Silverlight.VisualStudio.Design project.  The GetPlatformType method takes a platform neutral TypeIdentifier and returns a platform specific type.

 using System;
using CiderControls.Common.VisualStudio.Design.Infrastructure;
using CiderControls.Common.VisualStudio.Design.Registration;
using CiderControls.Silverlight.VisualStudio.Design.Types;
using Microsoft.Windows.Design.Metadata;

namespace CiderControls.Silverlight.VisualStudio.Design {

  internal class SilverlightTypeResolver : RegistrationTypeResolver {

    public override Type GetPlatformType(TypeIdentifier id) {
      switch (id.Name) {

        case Constants.STR_CIDERCONTROLSFEEDBACK:
          return SilverlightTypes.FeedbackControlType;

        case Constants.STR_CIDERCONTROLSRATING:
          return SilverlightTypes.RatingControlType;

      }

      throw new ArgumentOutOfRangeException("id.Name", id.Name, message...);
    }
  }
}

This assembly, CiderControls.Silverlight.VisualStudio.Design is a WPF assembly.  Notice that we are not directly returning a Silverlight type by using the typeof method.  Instead we are returning a type that is located in the CiderControls.Silverlight.VisualStudio.Design.Types assembly and is exposed by static properties in the SivlerlightTypes class below.

The CiderControls.Silverlight.VisualStudio.Design.Types assembly has a project reference to the Sivlerlight controls run-time assembly.

 using System;

[assembly:
  System.Runtime.CompilerServices.InternalsVisibleTo("CiderControls.Silverlight.VisualStudio.Design")]

[assembly:
  System.Runtime.CompilerServices.InternalsVisibleTo("CiderControls.Common.VisualStudio.Design")]

namespace CiderControls.Silverlight.VisualStudio.Design.Types {

  internal class SilverlightTypes {

    public static readonly Type FeedbackControlType = typeof(CiderControls.Feedback);
    public static readonly Type RatingControlType = typeof(CiderControls.Rating);
  }
}

I’ve made use of the Friend Assemblies feature that allows all my classes in the solution to be internal.  For VB.NET they will all be scoped as Friend.

Silverlight Type Resolution

By now you understand that the CiderControlsAttributeTableBuilder is “platform neutral” and is located in the CiderControls.Common.VisualStudio.Design assembly.

You know that the SiverlightTypeResolver will be pass into the CiderControlsAttributeTableBuilder constructor.  CiderControlsAttributeTableBuilder is the same class that the WPFTypeResolver will be passed into also.

When the CiderControlsAttributeTableBuilder needs to resolve a type it will pass a platform neutral TypeIdentifier to one of the TypeResolvers, they in turn will return a platform specific type.

For Silverlight only, the SiverlightTypeResolver will use the SilverlightTypes class to perform the actual type resolution.

While this seems confusing at first, it is the best way to allow Silverlight types to be resolved in a platform neutral fashion and without polluting a WPF assembly with Silverlight types.

Metadata Building 

CiderControlsAttributeTableBuilder does the heavy lifting and is responsible for creating the AttributeTable required by Visual Studio.  This code is platform neutral, meaning it returns an AttributeTable for WPF or Silverlight without having to directly reference the run-time assembly controls inside its code.

For the below code, I left the comments in place to make reading a large section of code much easier.  The comments inline explain what is going on.

You’ll notice how the WPFTypeResolver and the SiverlightTypeResolver are used in this code.  The module level variable _registrationTypeResolver holds a reference to the TypeResolver that is used to resolve platform specific types using the platform neutral TypeIdentifier.

 using System;
using System.ComponentModel;
using CiderControls.Common.VisualStudio.Design.Controls;
using CiderControls.Common.VisualStudio.Design.Infrastructure;
using Microsoft.Windows.Design.Features;
using Microsoft.Windows.Design.Metadata;
using Microsoft.Windows.Design.PropertyEditing;
using Microsoft.Windows.Design;

//
// This should be in AssemblyInfo.cs - declaring here to make it obvious
//

[assembly:
  System.Runtime.CompilerServices.InternalsVisibleTo("CiderControls.WPF.VisualStudio.Design")]

[assembly:
  System.Runtime.CompilerServices.InternalsVisibleTo("CiderControls.Silverlight.VisualStudio.Design")]

namespace CiderControls.Common.VisualStudio.Design.Registration {

  //TODO  5 - CiderControlsAttributeTableBuilder

  /// <summary>
  /// Platform neutral class that builds platform specific metadata
  /// </summary>
  internal class CiderControlsAttributeTableBuilder : AttributeTableBuilder {

    // allows for Platform specific Types to be used without directly referencing Silverlight
    private RegistrationTypeResolver _registrationTypeResolver;

    //TODO  6 - registrationTypeResolver resolves the platform specific Types in the platform neutral 
    //          metadata builder
    public CiderControlsAttributeTableBuilder(RegistrationTypeResolver registrationTypeResolver) {
      _registrationTypeResolver = registrationTypeResolver;

      AddFeedbackControlAttributes();
      AddRatingControlAttributes();
    }

    /// <summary>
    /// Builds Feedback control metedata
    /// </summary>
    private void AddFeedbackControlAttributes() {

      //TODO  7 - Resolve the Platform specific Feedback control from the platform neutral 
      //          TypeIdentifier
      Type feebackType = _registrationTypeResolver.GetPlatformType(MyPlatformTypes.Feedback.TypeId);

      //TODO  9 - Using the above feedbackType, create platform specific metadata for the Feedback 
      //          control the below code adds features, catetory editor, inline editor, type converter
      //          and assigns a Category to each Feedback control property

      AddTypeAttributes(feebackType,
          new FeatureAttribute(typeof(FeedbackControlInitializer)),
          new FeatureAttribute(typeof(FeedbackControlContextMenuProvider)),
          new FeatureAttribute(typeof(FeedbackControlAdornerProvider))
          );

      AddCategoryEditor(feebackType,
          typeof(FeedbackControlCategoryEditor));

      AddMemberAttributes(feebackType,
          Constants.STR_CORNERRADIUS,
          new CategoryAttribute(Constants.STR_COMMON));

      // since the below Header property is of type object, it requires this 
      // StringConverter to enable editing of simple string values in the properties window.
      AddMemberAttributes(feebackType,
          Constants.STR_HEADER,
          new CategoryAttribute(Constants.STR_CUSTOM),
          new TypeConverterAttribute(typeof(StringConverter)));

      AddMemberAttributes(feebackType,
          Constants.STR_VALUE,
          new CategoryAttribute(Constants.STR_CUSTOM),
          PropertyValueEditor.CreateEditorAttribute(typeof(RatingSelectorInlineEditor)));

      AddMemberAttributes(feebackType,
          Constants.STR_COMMENT,
          new CategoryAttribute(Constants.STR_CUSTOM));

      AddMemberAttributes(feebackType,
          Constants.STR_COMMENTHEADING,
          new CategoryAttribute(Constants.STR_CUSTOM));

    }

    /// <summary>
    /// Builds Rating control metedata
    /// </summary>
    private void AddRatingControlAttributes() {

      //TODO  7.1 - Resolve the Platform specific Rating control from the platform neutral 
      //            TypeIdentifier
      Type ratingType = _registrationTypeResolver.GetPlatformType(MyPlatformTypes.Rating.TypeId);

      //TODO  9.1 - Using the above ratingType, create platform specific metadata for the Rating 
      //            control the below code keeps the Rating control from appearing in the 
      //            Choose Items dialog after the CiderControls.WPF or CiderControls.Siverlight 
      //            assembly is added.
      //
      // this is only done for illustration, there is no actual reason to keep the Rating control
      // out of the Choose Items dialog.
      AddTypeAttributes(ratingType,
          new ToolboxBrowsableAttribute(false)
          );
    }

    private void AddTypeAttributes(Type type, params Attribute[] attribs) {
      base.AddCallback(type, builder => builder.AddCustomAttributes(attribs));
    }

    private void AddCategoryEditor(Type type, Type editorType) {
      base.AddCallback(type, builder => 
         builder.AddCustomAttributes(CategoryEditor.CreateEditorAttribute(editorType)));
    }

    private void AddMemberAttributes(Type type, string memberName, params Attribute[] attribs) {
      base.AddCallback(type, builder => builder.AddCustomAttributes(memberName, attribs));
    }
  }
}

The last three methods are helper methods that make the metadata code cleaner and easier to read.

In the next Part II of this article, I’ll tie the above attributes to each of the design-time features.

Downloads 

C# Source Code Download

VB.NET Source Code Download

PowerPoint Slides from the Development Tools Ecosystem Summit Presentation

Online Documentation Status 

Almost all of MSDN and most examples you’ll see on the Internet use the interfaces from Visual Studio 2008.  When you see IRegisterMetadata used in an example, you know you’re looking at old code.  Overtime this will change, new examples will be posted and MSDN updated.

Remember IRegisterMetadata has been replaced with IProvideAttributeTable in Visual Studio 2010.  Most of old the code and the substance of the example or documentation you are viewing is probably correct and good to learn from.  You just need to be aware that you are looking at older code and that example won’t compile under Visual Studio 2010.

More Information Links 

MSDN: WPF Designer Extensibility  (This is the best extensibility reference.)

WPF and Silverlight Designer Extensibility Samples

Ning Zhang's Blog: Silverlight Design Time: Toolkit October 2009 Release Update

How to Write Silverlight Design Time for All Designers: Visual Studio 2008, Blend 2; Blend 3, and Visual Studio 2010

Comments 

Microsoft values your opinion about our products and documentation. In addition to your general feedback it is very helpful to understand:

  • How the above feature enables your workflow
  • What is missing from the above feature that would be helpful to you

Thank you for your feedback and have a great day,

Karl Shifflett

Visual Studio Cider Team