Dotneteers.net
All for .net, .net for all!

LearnVSXNow! #16 - Creating a simple custom editor — the first ten meter

With my professional dive master certification I am allowed to dive not deeper than 42 meters (about 130 feet) under the water. On the surface the normal pressure is 1 bar (15 PSI), and every 10 meter (about 30 feet) the pressure is increasing with one bar (15 PSI). So, at 10 meter it is 2 bars (30 PSI), at 40 meter (120 feet) it is 5 bars (75 PSI). Just to have a comparison: within a car’s tire the pressure is 2 bars (30 PSI), the same that we feel at ten meter. There is very different what we feel at 10 meter and 40 meter. If you do not used to dive down to 10 meter, you are unlikely to dive to 40. Why am I telling this?

In the previous article (Part #15) I treated the architecture basics of custom editors in Visual Studio. To make the basics more easy to understand I have created a BlogItemEditor sample and showed most common elements. I feel it was like a dive to 5 meter (15 feet); you even do not need scuba equipment for that. In this part we start with the registration of the BlogItemEditor, than we move to the details of the editor factory, the data and the UI and then to the upper layer of the editor pane. It is like diving down to 10 meter (30 feet). In the next article me descend under 40 (120 feet)!

Registering editors

Now, all of us know that any objects used in packages should be registered in order to be available in Visual Studio. In case of our simple editor (and in case of many custom editors) registration actually affects three entities:

—  The editor factory. This object is the key entity to create the editors so we must tell Visual Studio which factory to use.

—  File extension for the file type our editor supports. Without providing a registered file extension we theoretically could use our editor by simply adding a file with the right extension to the project. However, generally we cannot do anything with an empty file: using a file template (a file with predefined content) would help us a lot.

—  A logical view for our custom editor. (In a few paragraphs later I will tell you what a logical view is)

Code for registration

To register our editor with all the “mandatory” elements we use attributes decorating our package class. However, we must pass a concrete editor factory instance to Visual Studio in order to create editor instances:

// --- Other attributes have been omitted

[ProvideEditorFactory(typeof(BlogItemEditorFactory), 200,

  TrustLevel = __VSEDITORTRUSTLEVEL.ETL_AlwaysTrusted)]

[ProvideEditorExtension(typeof(BlogItemEditorFactory),

  HowToPackage.BlogFileExtension,

  32,

  ProjectGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}",

  TemplateDir = @"..\..\BlogItemEditor\Templates",

  NameResourceID = 200)]

[ProvideEditorLogicalView(typeof(BlogItemEditorFactory),

  GuidList.GuidBlogItemEditorLogicalView)]

public sealed class HowToPackage : Package

{

  public const string BlogFileExtension = ".blit";

 

  protected override void Initialize()

  {

    base.Initialize();

    // --- Other initialization code

 

    // --- Register the blog item editor

    RegisterEditorFactory(new BlogItemEditorFactory());

   

    // --- Other initialization code

  }

}

The ProvideEditorFactory attribute is used to define the type responsible for managing our editor. The magic number of 200 the resource ID containing the name for our editor factory. The even more magical TrustLevel does what its name suggests: sets the trust level for the editor.

The ProvideLogicalView method sets our factory as one that can provide a logical view. The GUID passed as the second argument is an identifier for the logical view.

The longest of the decorator attributes is the ProvideEditorExtension attribute that a takes six parameters (in my implementation).

—  The first parameter is the type of editor factory managing the instantiation of the custom editor.

—  The second parameter is the file extension used by the editor. In our case this is “.blit”.

—  The third parameter is the priority of the editor registered.

—  The ProjectGuid property sets the GUID of the project type we want the editor file type to be shown. In the example above the GUID is for the C# language projects. If we open a C# project, the editor is accessible with the Add New Item function; otherwise not.

—  The TemplateDir property sets the folder to look for template definition. Since the attribute is used by the RegPkg.exe started by MSBuild, the folder here is relative to the project file’s path.

—  The NameResourceID property sets the ID of the resource providing a name for file type represented by the extension.

The TemplateDir folder may contain a few file types recognized by the ProvideEditorExtension attribute (at registration type). I used the .vsdir file that contains one text line with fields separated by vertical bars “|”:

BlogItem.blit|{0380775d-5735-43ed-8c23-c1fda451e1c8}|#200|32|#202|

{0380775d-5735-43ed-8c23-c1fda451e1c8}|400|0|#203

(Please note that in the code above I put those fields into two lines but they are in one text line within the .vsdir file.). Fields from left to right have the following meaning:

—  BlogItem.blit: A file template for our editor. This file is also in TemplateDir and represents a startup file for the BlogItemEditor.

—  GUID: The GUID of our VSPackage.

—  #200: The resource ID used as a name for our file type.

—  32: Display ordering priority

—  #202: Description resource ID for our file type

—  GUID: The GUID for the resource .dll. We use here our package ID.

—  400: Resource ID for the icon representing the file type in the Add New Item dialog.

—  0: Various flags (I do not looked after what they are)

—  #203: The resource ID for the default file name.

There are some other alternatives beside the .vsdir file for templates, but I am not going to treat them in this article.

The registration attributes are not enough to make our editor work: we must register the factory instance that actually creates the concrete editor instances. It is the task of the RegisterEditorFactory method of the Package class. The only parameter it takes is the factory instance. As you see, this single instance will be the one creating the editors.

The editor factory

The life of our editor starts when Visual Studio requests the editor factory to create an editor of our own type. This is an instance of BlogItemEditorFactory.

Now, let’s look behind the code of BlogItemEditorFactory! As we saw in the previous article (Part #15), BlogItemEditorFactory simply inherits from the SimpleEditorFactory<> class with defining its own Guid and without adding any new functionality:

[Guid(GuidList.GuidBlogEditorFactoryString)]

public sealed class BlogItemEditorFactory:

  SimpleEditorFactory<BlogItemEditorPane>

{

  // --- That is the full code of this class! Nothing is omitted...

}

All the details are within the declaration body of SimpeEditorFactory:

public class SimpleEditorFactory<TEditorPane> :

  IVsEditorFactory,

  IDisposable

  where TEditorPane:

    WindowPane, IOleCommandTarget, IVsPersistDocData, IPersistFileFormat, new()

{

  private ServiceProvider _ServiceProvider;

 

  public SimpleEditorFactory() { ... }

 

  // --- IDisposable pattern implementation

  public void Dispose() { ... }

  private void Dispose(bool disposing) { ... }

 

  // --- IVsEditorFactory implementation

  public virtual int SetSite(IOleServiceProvider serviceProvide) { ... }

  public virtual int MapLogicalView(ref Guid logicalView, out string physicalView)

  { ... }

  public virtual int Close() { ... }

  [EnvironmentPermission(SecurityAction.Demand, Unrestricted = true)]

  public virtual int CreateEditorInstance(

      uint grfCreateDoc,

      string pszMkDocument,

      string pszPhysicalView,

      IVsHierarchy pvHier,

      uint itemid,

      IntPtr punkDocDataExisting,

      out IntPtr ppunkDocView,

      out IntPtr ppunkDocData,

      out string pbstrEditorCaption,

      out Guid pguidCmdUI,

      out int pgrfCDW)

    { ... }

 

  // --- Helper methods  

  public object GetService(Type serviceType) { ... }

}

The SimpleEditorFactory accepts a type parameter called TEditorPane. We expect this type to provide all the key services for an editor and so we require it to inherit from WindowPane and to implement IOleCommandTarget, IVsPersistDocData and IPersistFileFormat. To make our class a real editor factory we must implement IVsEditorFactory. Since in the implementation we site our factory class within Visual Studio by using a ServiceProvider instance, we must take care of its cleanup, so we implement the IDisposable interface. In this implementation I made all IVsEditorFactory members virtual in order to allow easy customization by inheritance.

When Visual Studio creates our editor factory, it sites it providing access to services:

public virtual int SetSite(IOleServiceProvider serviceProvider)

{

  _ServiceProvider = new ServiceProvider(serviceProvider);

  return VSConstants.S_OK;

}

Our code simply stores this IOleServiceProvider instance in the form of a ServiceProvider instance. When our factory would like to access VS services, it can call the GetService method:

public object GetService(Type serviceType)

{

  return _ServiceProvider.GetService(serviceType);

}

In the BlogItemEditor example we do not use this method. You can even pass the _ServiceProvider field to the document data and view at creation time to obtain service instances. Since we created an instance of ServiceProvider that is IDisposable, we define cleanup method with the standard IDisposable pattern to release resources used by _ServiceProvider:

public void Dispose()

{

  Dispose(true);

}

 

private void Dispose(bool disposing)

{

  if (disposing)

  {

    // --- Here we dispose all managed and unmanaged resources

    if (_ServiceProvider != null)

    {

      _ServiceProvider.Dispose();

      _ServiceProvider = null;

    }

  }

}

Beside the Dispose pattern we have the Close method to clean up everything we have created when setting up our factory. In our case we do not have anything to clean up:

public virtual int Close()

{

  return VSConstants.S_OK;

}

Now we arrived to the part of the IVsEditorFactory that does the real work. Before going on, I have to explain ideas not treated yet: the concept of logical view and physical view. When interacting with an editor (or better to imagine a designer) we use concrete instance of a view called physical view. If our designer supports more than one view, those can be grouped into logical categories. For example, our designer could have a view to see the information as text or as code; it may provide a different view while we are debugging. When creating a physical view, the shell offers the possibility to map it to a logical view and retrieve a name for the physical view that can be used as a parameter when creating the physical view instance. This is the role of the MapLogicalView method. In our example we have only one view for the BlogItemEditor, so our implementation of MapLogicalView looks like that:

public virtual int MapLogicalView(ref Guid logicalView, out string physicalView)

{

  physicalView = null;

  if (VSConstants.LOGVIEWID_Primary == logicalView)

  {

    // --- Primary view uses null as physicalView

    return VSConstants.S_OK;

  }

  else

  {

    // --- You must return E_NOTIMPL for any unrecognized logicalView values

    return VSConstants.E_NOTIMPL;

  }

}

The LOGVIEWID_Primary GUID asks for the primary view supported by our editor. If we got it, we return a null value as the name of the physical view (we will not use this information when instantiating the physical view) along with the S_OK status code. Since this is the only view we handle, requests for any other kind of view result in E_NOTIMPL status code. If we intended to create an editor with multiple views, this method should have been modified accordingly.

The real instantiation of the physical view is done in the CreateEditorInstance method that has a large number of arguments:

public virtual int CreateEditorInstance(

  uint grfCreateDoc,

  string pszMkDocument,

  string pszPhysicalView,

  IVsHierarchy pvHier,

  uint itemid,

  IntPtr punkDocDataExisting,

  out IntPtr ppunkDocView,

  out IntPtr ppunkDocData,

  out string pbstrEditorCaption,

  out Guid pguidCmdUI,

  out int pgrfCDW)

{ ... }

To understand them, I make a short summary in the following table:

Argument Description
grfCreateDoc

This argument represents flags determining when to create the editor. You can find these flags among the VSConstants fields with CEF_ prefix. Only the CEF_OPENFILE and CEF_SILENT are valid in this operation.

pszMkDocument

This argument is the full path to the file to be opened for the editor.

pszPhysicalView

Names the physical view (remember to MapLogicalView, in our case this argument is null.)

pvHier

Points to an IVsHierarchy item that holds the file or project item representing the file for our editor.

itemid

Identifies the item in the hierarchy pointed by pvHier. We have not treated hierarchies yet. To understand what pvHier and itemid does, handle them together as a reference to the item in the Solution Explorer we are creating the editor for.

punkDocDataExisting

This argument is used to determine if a document buffer (DocData object) has already been created. The argument has a role in case of multiple-view editors that work over the same document data instance.

ppunkDocView

We retrieve the newly created document view instance in this argument. Since the caller expects a pointer to a COM object instance, we must marshal it.

ppunkDocData

We retrieve the newly created data view instance in this argument. Since the caller expects a pointer to a COM object instance, we must marshal it.

pbstrEditorCaption

The method retrieves the caption for the document window.

pguidCmdUI

This is the GUID of the command group represented by the elements of the editor. UI elements of the editor must be able to process commands belonging to this group.

pgrfCWD

Flags to influence document window creation. In our example we will not use these flags.

Now, let’s see the implementation of this method (I omit the method arguments):

public virtual int CreateEditorInstance

(

  // ... 

  // --- See arguments in the code above

)

{

  // --- Initialize to null

  ppunkDocView = IntPtr.Zero;

  ppunkDocData = IntPtr.Zero;

  pguidCmdUI = GetType().GUID;

  pgrfCDW = 0;

  pbstrEditorCaption = null;

 

  // --- Validate inputs

  if ((grfCreateDoc & (VSConstants.CEF_OPENFILE | VSConstants.CEF_SILENT)) == 0)

  {

    return VSConstants.E_INVALIDARG;

  }

  if (punkDocDataExisting != IntPtr.Zero)

  {

    return VSConstants.VS_E_INCOMPATIBLEDOCDATA;

  }

  // --- Create the Document (editor)

  TEditorPane newEditor = new TEditorPane();

  ppunkDocView = Marshal.GetIUnknownForObject(newEditor);

  ppunkDocData = Marshal.GetIUnknownForObject(newEditor);

  pbstrEditorCaption = "";

  return VSConstants.S_OK;

}

At the beginning of the method we initialize all output arguments. Almost all parameters are initialized to a “null-like” value, except pguidCmdUI. This one is initialized to the type GUID of the concrete editor factory. (Remember, our SimpleEditorFactory is a generic type. When we derived the BlogItemEditorFactory from it we declared a type GUID using a GuidAttribute.) The GUID value we used for the type GUID of the factory will be the GUID we use for the command set belonging to our editor.

We check if only valid grfCreateDoc flags are passed. In case of problems we return E_INVALIDARG status.

Since our document view and document data are instantiated here we do not accept already document data. If we got document data from the caller, we refuse it with the VS_E_INCOMPATIBLEDOCDATA status.

In the last part of the method we create a TEditorPane instance (remember, this type represents both our document view and data). Because the caller expects a pointer for IUnknown interface, we use the Marshal.GetIUnknownForObject interop method to create that pointer cast between the managed world and the COM world.

Document Memory Data: BlogItemEditorData

The BlogItemEditorFactory class creates instances of BlogItemEditorPane class that represents the document view and the document data of the editor. To explain how BlogItemEditorPane works, we’d better get familiar with the “memory part” of document data that is represented by a BlogItemEditorData instance. This class is quite simple (even if it has about 200 lines), since it is a class with three read-write properties and a few methods the save and load the XML file representation of the blog item data. The “header” of the class code defines fields, constants and XName values used for XML persistence and constructors:

public sealed class BlogItemEditorData : IXmlPersistable

{

  private string _Title;

  private string _Categories;

  private string _Body;

 

  public const string BlogItemNamespace =

    "http://www.codeplex.com/LearnVSXNow/BlogItemv1.0";

  public const string BlogItemLiteral = "BlogItem";

  // --- Other contant values used for XMLpersistence

 

  private readonly XName BlogItemXName = XName.Get(BlogItemLiteral, 

    BlogItemNamespace);

  // --- Other readonly fields representing XML elements of the .blit file

 

  public BlogItemEditorData()

  {

  }

    

  public BlogItemEditorData(string title, string categories, string body)

  {

    _Title = title;

    _Categories = categories;

    _Body = body;

  }

 

  // --- Read-write properties omitted

The XML persistence methods have been cut into two parts. The methods using a single fileName argument deal with file, methods using XElement arguments handle XML node instances for persistence. File methods internally use XElement-based methods:

  public void SaveTo(string fileName)

  {

    // --- Create the root document element

    XElement root = new XElement(BlogItemXName);

    XDocument objectDoc = new XDocument(root);

 

    // --- Save document data to XElement and then tofile

    SaveTo(root);

    objectDoc.Save(fileName);

  }

 

  public void ReadFrom(string fileName)

  {

    string fileContent = File.ReadAllText(fileName);

    XDocument objectDoc = XDocument.Parse(fileContent,

      LoadOptions.PreserveWhitespace);

 

    // --- Check the document element

    XElement root = objectDoc.Element(BlogItemXName);

    if (root == null)

      throw new InvalidOperationException(

        "Root '" + BlogItemLiteral + "' element cannot be found.");

     // --- Read the document

    ReadFrom(root);

  }

Thanks to the new LINQ for XML types in the System.XML.Linq namespace I had to write significantly less number of code lines than using the “traditional” XmlDocument related approach. To demonstrate this fact here is the code saving the blog item into a XElement:

  public void SaveTo(XElement targetElement)

  {

    // --- Create title

    targetElement.Add(new XElement(TitleXName, _Title));

    // --- Create category hierarchy

    XElement categories = new XElement(CategoriesXName);

    targetElement.Add(categories);

    string[] categoryList = _Categories.Split(';');

    foreach (string category in categoryList)

    {

      string trimmed = category.Trim();

      if (trimmed.Length > 0)

      {

        categories.Add(new XElement(CategoryXName, trimmed));

      }

    }

    // --- Create the body

    targetElement.Add(new XElement(BodyXName, new XCData(_Body)));

  }

The ReadFrom(XElement) method (I do not enclose the code) is also simple.

Document UI: BlogItemEditorControl

The user interface of the blog item editor is represented in a user control that is hosted in a Visual Studio window frame:

The User control contains very simple interaction logic, and it also implements the ICommonCommandSupport interface that is responsible for handling the most common editor commands:

public partial class BlogItemEditorControl :

  UserControl, ICommonCommandSupport { ... }

Do not search for ICommonCommandSupport in the reference guide since it is a creation of my own. It contains a few bool properties with Supports... prefix and their corresponding Do-pefixed methods.  BlogItemEditor control also contains methods to synchronize the data and the view:

public partial class BlogItemEditorControl :

  UserControl,

  ICommonCommandSupport

{

  public BlogItemEditorControl()

  {

    InitializeComponent();

  }

 

  public void RefreshView(BlogItemEditorData data)

  {

    TitleEdit.Text = data.Title ?? string.Empty;

    CategoriesEdit.Text = data.Categories ?? String.Empty;

    BodyEdit.Text = data.Body ?? String.Empty;

  }

 

  public void RefreshData(BlogItemEditorData data)

  {

    data.Title = TitleEdit.Text;

    data.Categories = CategoriesEdit.Text;

    data.Body = BodyEdit.Text;

  }

As you see RefreshView and RefreshData are used to synchronize the data and the view content of the editor. The Supports... methods return false for SelectAll, Redo and Undo since our control does not support them (well, you can implement them; I was too lazy to do so...)

// --- ICommonCommandSupport implementation

bool ICommonCommandSupport.SupportsSelectAll

{ get { return false; } }

bool ICommonCommandSupport.SupportsRedo

{ get { return false; } }

bool ICommonCommandSupport.SupportsUndo

{ get { return false; } }

The Copy and Cut commands are supported if there is an active control with a non-zero-length selection:

// --- ICommonCommandSupport implementation

bool ICommonCommandSupport.SupportsCopy

{

  get { return ActiveControlHasSelection; }

}

// ...

private bool ActiveControlHasSelection

{

  get

  {

    TextBox active = ActiveControl as TextBox;

    return active == null ? false : active.SelectionLength > 0;

  }

}

Paste command is supported, if there is text data on the clipboard and there is an active textbox to paste the clipboard content in:

// --- ICommonCommandSupport implementation

bool ICommonCommandSupport.SupportsPaste

{

  get { return ActiveCanPasteFromClipboard; }

}

// ...

private bool ActiveCanPasteFromClipboard

{

  get

  {

    TextBox active = ActiveControl as TextBox;

    return (active != null && Clipboard.ContainsText());

  }

}

It is not enough to declare whether we support a command or not, but also have to execute supported commands:

// --- ICommonCommandSupport implementation

void ICommonCommandSupport.DoCopy()

{

  TextBox active = ActiveControl as TextBox;

  if (active != null) active.Copy();

}

 

void ICommonCommandSupport.DoCut()

{

  TextBox active = ActiveControl as TextBox;

  if (active != null) active.Cut();

}

 

void ICommonCommandSupport.DoPaste()

{

  TextBox active = ActiveControl as TextBox;

  if (active != null) active.Paste();

}

Now, our user interface can respond to Visual Studio commands. However, we miss one very important aspect: when we edit the information we must know when our data gets “dirty” in the memory. The “dirtiness” is used by Visual Studio to indicate that our data has to be saved when the editor window is closed.

The user interface itself has to indicate when data is about to get dirty. Actually only textboxes get dirty and underlying data not, since textboxes are occasionally synchronized with data. However, we must tell the view data is getting dirty.

We use the good old events for this. Our user control will raise an event any time its content is changing. The view can subscribe to this event and be notified about “dirtiness”:

public event EventHandler ContentChanged;

 

private void RaiseContentChanged(object sender, EventArgs e)

{

  if (ContentChanged != null) ContentChanged.Invoke(sender, e);

}

 

private void ControlContentChanged(object sender, EventArgs e)

{

  RaiseContentChanged(sender, e);

}

We implement this patent with three simple members: ContentChanged is the public event to subscribe to; ControlContentChanged is assigned to the TextChanged event of all textboxes belonging tour user interface; RaiseContentChanged invokes the event handlers.

Removing the pain: BlogItemEditorPane

At this point we have got everything to go on with the toughest part of our custom editor: the BlogItemEditorPane class. If you think that you have seen too much code in this article, I have a bad news for you: all the code before was only a small part of what we have behind. When starting the examination of the VS 2008 SDK sample C# Example.EditorWithToolbox I also felt there is too much (too long) code inside for such a “small” thing like a custom editor. But as I went deeper and deeper into the sample code I had to realize that is the nature of custom editors: there are many “small” details that result in a long code. If some solution requires a long code we can factor out those things that can be patterns for specific situations. I have recognized that more than 90% of the example code is really a pattern for simple editors (like the BlogItemEditor). So after having that refactoring done, I had a really short code for my BlogItemEditorPane class.

Just to remember you, what is the role of the BlogItemEditorPane, here is the figure from Part #15 again:

BlogItemEditorPane is the class that implements the majority of the document data and document view roles. We have the BlogItemEditorData class, but it represents the memory footprint of the editor data and adds a few methods to persist the data into XML files. BlogItemEditorControl provides only the UI part of the view. BlogItemEditorPane is the class that undertakes controller-like functions (by MVC meanings) and coordinates among the shell, the view and the data.

So here is the full code for the BlogItemEditorPane (sorry, I omitted inline comments for readability):

public sealed class BlogItemEditorPane:

  SimpleEditorPane<BlogItemEditorFactory, BlogItemEditorControl>

{

  private readonly BlogItemEditorData _EditorData = new BlogItemEditorData();

 

  public BlogItemEditorPane()

  {

    UIControl.ContentChanged += DataChangedInView;

  }

 

  protected override string GetFileExtension()

  {

    return HowToPackage.BlogFileExtension; // --- “.blit”

  }

 

  protected override Guid GetCommandSetGuid()

  {

    return GuidList.GuidBlogEditorCmdSet;

  }

 

  protected override void LoadFile(string fileName)

  {

    _EditorData.ReadFrom(fileName);

    UIControl.RefreshView(_EditorData);

  }

 

  protected override void SaveFile(string fileName)

  {

    UIControl.RefreshData(_EditorData);

    _EditorData.SaveTo(fileName);

  }

 

  void DataChangedInView(object sender, EventArgs e)

  {

    OnContentChanged();

  }

}

That’s all. That code implements all the behavior I expect from a custom editor. I you write a custom editor having similar complexity in its user interface and interaction model, you have to write about the same amount of code (less that 50 lines without comments). Let’s see what it does:

_EditorData field represents the memory footprint of our data. We use this field in the SaveFile method (to pull the data from the UI into the memory before save) and LoadFile method (to refresh the UI after we load the data into the memory).

In the instance constructor the pane subscribes for the ContentChanged event of the BlogItemEditorControl. When the event fires, the DataChangedInView event handler method calls the OnContentChanged method of the base class and notifies the shell.

We have left two mandatory methods to implement. First is GetFileExtension to tell the pane “.blit” is the file extension we use. Second is GetCommandSetGuid to tell the SimpleEditorPane class behind what command group belongs to the editor (what are the commands the editor recognizes as its own).

There is no free lunch. We cannot simply drop code. Nor can I. The code that makes BlogItemEditorControl so compact is moved into the SimpleEditor<,> generic class. 

So, check your pressure gauge! Prepare for the 40 meter (120 feet) dive: in the next article—concluding simple editors—I will examine that code.

 

 


Posted Mar 14 2008, 08:00 AM by inovak
Filed under:

Comments

GA30 wrote re: LearnVSXNow! #16 - Creating a simple custom editor — the first ten meter
on Mon, May 18 2009 3:42

I was wondering what you think the best way would be to use the framework but at the same time make my editor control a WPF user control rather than a windows forms user control. Currently the framework restricts the type parameter TUIControl to be of type System.Windows.Control. Without modifying the framework I believe my only option would be to continue using a windows forms control and just simply host a WPF control inside it. On the other hand, if I modify the framework, which I'd rather not do if possible, then I think I would have to change the type parameter restriction from System.Windows.Forms to simply IWin32Window and have my WPF control somehow implement a Handle property. I may go this route even if it means modifying the framework just because I would prefer not having to delegate the entire ICommonEditorCommand service from the windows forms user control to the WPF user control since EditorPaneBase tries to cast the control to the command commands. Is there any deeper reason why I should not modify the framework? Thank you as always for all the invaluable info

GA30 wrote re: LearnVSXNow! #16 - Creating a simple custom editor — the first ten meter
on Mon, May 18 2009 5:26

Please disregard question above.

Peter wrote re: LearnVSXNow! #16 - Creating a simple custom editor — the first ten meter
on Sat, Dec 12 2009 19:49

The article is great but without the images it's half the fun :(

????????????LearnVSXNow! #16- ????????????????????????-2 » NoName wrote ????????????LearnVSXNow! #16- ????????????????????????-2 &raquo; NoName
on Sat, Jun 11 2011 12:55

Pingback from  ????????????LearnVSXNow! #16- ????????????????????????-2 » NoName

????????????LearnVSXNow!#16-????????????????????????-2 - ??????????????? - ????????? wrote ????????????LearnVSXNow!#16-????????????????????????-2 - ??????????????? - ?????????
on Tue, Apr 10 2012 6:41

Pingback from  ????????????LearnVSXNow!#16-????????????????????????-2 - ??????????????? - ?????????

Topsoil Supplies wrote re: LearnVSXNow! #16 - Creating a simple custom editor — the first ten meter
on Sat, Feb 16 2013 12:26

I really like and appreciate your blog.Really thank you! Really Great.