Editing Design-Time Properties with a Custom Modal Dialog

This is the last post in our series about implementing a custom pipeline component used to dynamically resolve and select an appropriate flat file schema for disassembling incoming BizTalk messages.

At this stage, we have a fully functional pipeline component. However, we would like to make its configuration straightforward and easy in the Visual Studio pipeline designer.

User-Friendly Configuration Dialog

First we’ll add a custom Windows Forms, whose purpose is to present a user-friendly way for discovering the registered Schema Resolvers and select the ones we would like to use for this instance of the pipeline.

The purpose of this post is not to show to full source code for this custom form. However, let’s illustrate some key bits…

The purpose of the form is to update a list of Resolver components. Before the form is displayed, a (possibly non empty) list of Resolvers is supplied. This list will be updated by the user has she manipulates the various controls.

public partial class ResolverNameCollectionEditor : Form
{
    #region Data Members

    private ResolverNameCollection collection_ = new ResolverNameCollection();

    #endregion

    #region Attributes

    public ResolverNameCollection Resolvers
    {
        get { return collection_; }
        set { collection_ = value; }
    }

    #endregion

First, upon loading, the form iterates over the set of registered Resolvers by inspecting the known registry key under which they must be installed. These resolvers are then displayed with a ListView control on the top portion of the dialog.

    #region Constants

    private const String RegistrySubkey = @"SOFTWARE\BizTalk Factory\Pipeline Components\Schema Resolvers";

    #endregion

    #region Attributes

    private static ResolverNameCollection RegisteredResolvers
    {
        get
        {
            ResolverNameCollection resolvers = new ResolverNameCollection();

            RegistryKey hKey = Registry.LocalMachine.OpenSubKey(RegistrySubkey, false);
            foreach (string name in hKey.GetSubKeyNames())
            {
                RegistryKey hSubKey = hKey.OpenSubKey(name);
                resolvers.Add(new ResolverName(hSubKey.GetValue("Assembly") as string));
            }

            return resolvers;
        }
    }

    #endregion

    #region Event Handlers

    private void ResolverNameCollectionEditor_Load(object sender, EventArgs e)
    {
        InitializeRegisteredResolvers();
        InitializeSelectedResolvers();
        UpdateControlState();
    }

    #endregion

    #region Implementation

    private void InitializeRegisteredResolvers()
    {
        foreach (ResolverName resolverName in RegisteredResolvers)
        {
            ListViewItem item = new ListViewItem();

            Type type = Type.GetType(resolverName.QualifiedName);
            AssemblyName assemblyName = new AssemblyName(type.Assembly.FullName);
            item.Text = type.Name;
            item.Tag = new ListViewItemTag(resolverName);

            item.SubItems.Add(new ListViewItem.ListViewSubItem(item, assemblyName.Name));
            item.SubItems.Add(new ListViewItem.ListViewSubItem(item, assemblyName.Version.ToString()));
            item.SubItems.Add(new ListViewItem.ListViewSubItem(item, GetCultureName(assemblyName.CultureInfo)));
            item.SubItems.Add(new ListViewItem.ListViewSubItem(item, GetHexString(assemblyName.GetPublicKeyToken())));

            vwResolvers.Items.Add(item);
        }
    }

    #endregion

Since we want to keep track of which Resolver has been selected, each item in the ListView is associated with a custom ListViewItemTag class that holds its selected state, depending upon whether this Resolver appears in the initial list or not.

    private class ListViewItemTag
    {
        public ResolverName ResolverName;
        public bool Selected;

        public ListViewItemTag(ResolverName resolverName)
        {
            ResolverName = resolverName;
            Selected = false;
        }
    }

Next, the form will display all Resolvers from the supplied list into the ListBox control on the bottom portion of the screen. Those represent the set of Resolvers used for any given instance of the pipeline component.

    private void InitializeSelectedResolvers()
    {
        foreach (ListViewItem item in vwResolvers.Items)
            if (Resolvers.Contains((item.Tag as ListViewItemTag).ResolverName))
                SelectItem(item);
    }

Each selected Resolver is displayed in a disabled state (grayed out) to provide more appropriate visual feedback.

Now, each time a Resolver component is selected from the top ListView control to the bottom ListBox control, its selected state and visual representation is updated accordingly, and a new line is added to the bottom ListBox control to reflect the selection of a new Resolver.

    private void SelectItem(ListViewItem item)
    {
        ListViewItemTag tag = (ListViewItemTag)item.Tag;
        if (!tag.Selected)
        {
            tag.Selected = true;
            item.ForeColor = SystemColors.GrayText;
            item.Selected = false;

            lstResolvers.Items.Add(tag.ResolverName.QualifiedName);
        }
    }

Conversely, each time a Resolver component is removed, its associated line from the bottom ListBox is deleted, and its selected state and visual representation in the top ListView control are updated to reflect this.

    private void UnselectCurrentItem()
    {
        string qualifiedName = (lstResolvers.SelectedItem as String);
        lstResolvers.Items.RemoveAt(lstResolvers.SelectedIndex);

        foreach (ListViewItem item in vwResolvers.Items)
        {
            ListViewItemTag tag = (ListViewItemTag)item.Tag;
            if (tag.Selected && tag.ResolverName.QualifiedName == qualifiedName)
            {
                tag.Selected = false;
                item.ForeColor = ListViewItemSystemColors.WindowText;
                item.Selected = true;
                vwResolvers.Select();
                break;
            }
        }
    }

Custom Editor for the Design-Time Properties

The custom dialog box illustrated in this post needs to be wired up to the Visual Studio property grid implementation so that, when a user wants to update the list of Resolvers to use for a particular pipeline, our custom form is displayed.

For this, we need to include a custom UITypeEditor like so:

public class ResolverNameModalEditor : UITypeEditor
{
    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        if (context == null || context.Instance == null)
            return base.GetEditStyle(context);

        return UITypeEditorEditStyle.Modal;
    }

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        System.Diagnostics.Debug.Assert(value is ResolverNameCollection);

        IWindowsFormsEditorService editorService;

        if (context == null || context.Instance == null || provider == null)
            return value;

        editorService = (IWindowsFormsEditorService)
               provider.GetService(typeof(IWindowsFormsEditorService));

        ResolverNameCollectionEditor CollectionEditor = new ResolverNameCollectionEditor();
        CollectionEditor.Resolvers = (value as ResolverNameCollection);

        if (editorService.ShowDialog(CollectionEditor) == System.Windows.Forms.DialogResult.OK)
            return CollectionEditor.Resolvers;

        return value;
    }
}

Notice that the GetEditStyle method instructs the Visual Studio property grid that we want to display a modal dialog.

Visual Studio will then call the EditValue to trigger the display of the custom form. First, a new instance of the custom modal form is created and the current value for the design-time property is supplied so that the correct initialization takes place upon loading. Then, the modal form is displayed indirectly by calling the ShowDialog method of the IWindowsFormsEditorService implementation retrieved from the Visual Studio.

Finally, to instruct Visual Studio to use our custom editor for any modification to the ResolverNames collection, the property must be decorated with additional attributes like so:

  [System.ComponentModel.Browsable(true)]
  [System.ComponentModel.Editor(typeof(UI.ResolverNameModalEditor), typeof(System.Drawing.Design.UITypeEditor))]
  public UI.ResolverNameCollection ResolverNames
  {
    get { return resolvers_; }
    set { resolvers_ = value; }
  }

Conclusion

This concludes the series on the custom SchemaResolveDisassembler pipeline component. My goal in writing those posts was to illustrate and end-to-end scenario by focusing on tasks that are necessary above and beyond the strict pipeline component code, in order to provide a robust component suitable for use in production.

This entry was posted in Pipeline Components, Tips. Bookmark the permalink.

3 Responses to Editing Design-Time Properties with a Custom Modal Dialog

  1. Haeussler, Wolfgang says:

    Hi Maxime,

    thanks for this great series, I appreciate it very much. It’s for sure the most complete and best documented work about pipeline components on the web and it’s way better than the samples in the SDK.
    Is there a chance that you could please explain how you have structured your VS solution and projects and how you have organized your .NET namespaces?

    Regards,
    Wolfgang

    • Hi Wolfgang,

      Thanks a lot for your feedback.

      In fact, all my pipeline components use the exact same PipelineComponentBase base class shown on this blog. You can download it too, it has been in used flawlessly in many production solutions for more than three years now.

      As you know, virtually all pipeline components use some form of System.IO.Stream-derived classes. Therefore, I also have a centralized “BizTalkFactory.Streaming” assembly, where all the underlying stream classes are implemented. This allows me to unit-test them independantly from the components themselves or, why not, from other utility projects. Also, this does not have any impact on the deployment side of things, since in case of maintenance, I can just re-GAC this assembly and all components benefit from the updated classes the next time they run.

      Then we have the pipeline components themselves. Each one is located in a separate assembly, but they all share the same namespace “BizTalkFactory.Component”. The main class that implements the component is there. However, custom type editors and type converters are in a “.UI” nested namespace.

      I tend to *not* GAC individual components. Instead they live in the dedicated “%BIZTALK%\Pipeline Components” subfolder and they can be replaced when needed. I have my installation/deployment scripts copy the components automatically in this location as part of their processings.

      I think that sums it up.
      Do not hesitate to contact me if you need more details.

      Cheers.

      • sri chaitanya says:

        Hi maxie,
        I have the same requirment to dynamically resolve schema in my project, Your article looks great but when it comes to implementing your code into my solution its not working for me…I just from your articles and trying to build it but its failing… can you please share your code? really appreciate your help

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s