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

LearnVSXNow! #17 - Creating a simple custom editor — under pressure

If you are a new reader of the LearnVSXNow! series, you are just in the middle of a three-part topic about custom editors. This part itself will not give you valuable information without reading Part #15 and #16, so, please, first look at them...

As I wrote in the previous article in our virtual dive we have already descended to 10 meters (30 feet) under the surface when we went into the code of the BlogItemEditor example. In this article we are going to reach the bottom depth (at 40 meter, 120 feet). This whole article is about the SimpleEditorPane<,> generic class that implements the majority of concepts behind a simple—and I really would like to emphasize the word “simple”—custom editor.

When refactoring the editor pane class I wanted to provide a simple design where only a few methods should be redefined (overridden) in the derived classes. I also wanted to leave the base implementation open for advanced customization. I have created a few abstract methods and about a dozen virtual methods.

SimpleEditorPane: data view and data document roles

The set of functions (services) I factored out from the Visual Studio 2008 SDK samples C# Example.EditorWithToolbox are around the details a document view and a document data should provide for a custom editor. The definition of the class is the following:

  public abstract class SimpleEditorPane<TFactory, TUIControl> :

    WindowPane,

    IOleCommandTarget,

    IVsPersistDocData,

    IPersistFileFormat

    where TFactory: IVsEditorFactory

    where TUIControl: Control, ICommonCommandSupport, new()

{

}

As you see, the class is abstract so you cannot use it directly. It accepts two type parameters. TFactory is an argument for a type acting as a factory for our editor and so we expect it implements IVsEditorFactory. One implementation is the BlogItemEditorFactory class I treated in Part #16. The second type argument is a placeholder for the user control representing the UI handling user interactions. This UI implements only a small part of the behavior we expect from a document view. An important role of this UI is the ability to respond standard commands like Cut, Copy and Paste via implementing the ICommonCommandSupport interface.

Our class inherits from WindowPane that implements a few interfaces. In our main focus is IVsWinowPane and IOleCommandTarget among them. WindowPane provides us the window management services like docking, dynamic placement of windows and many more. SimpleEditorPane also implements IOleCommandTarget which is required to be able to process standard commands and provide our own commands. IVsPersistDocData is responsible for persisting document data and providing the related controller functions, while IPersistFileFormat is responsible to manage different file formats for the editor.

Storing and accessing state

There are a few fields representing the state of our editor pane:

private TUIControl _UIControl;

private readonly string _FileExtensionUsed;

private readonly Guid _CommandSetGuid;

private string _FileName;

private bool _IsDirty;

private bool _Loading;

private bool _GettingCheckoutStatus;

private bool _NoScribbleMode;

_UIControl holds an instance of the user control representing the user interface of our editor pane. _FileExtensionUsed and _CommandSetGuid is initialized at construction time. They store the file extension used by our editor and the logical command set GUID.  These fields can be accessed through the FileExtensionUsed and CommandSetGuid properties. The derived classes must provide values for these state variables by overriding the abstract GetFileExtensionUsed and GetCommandSetGuid functions. We store the full path of the editor file in the _FileName field.

The Boolean _IsDirty field has an important role: we use it to tell Visual Studio Shell that our document has been modified within the editor. The remaining flags (_Loading,  _GettingCheckoutStatus and _NoScribbleMode) are used by the internal logic, we treat them later.

Initializing and handling the UI

The SimpleEditorPane delegates the responsibility of UI to a user control that will be hosted in a window pane. We take care of this UI at initialization and at finalization time:

protected SimpleEditorPane() : base(null)

{

  _FileExtensionUsed = GetFileExtension();

  _CommandSetGuid = GetCommandSetGuid();

  _UIControl = new TUIControl();

}

 

protected override void Dispose(bool disposing)

{

  try

  {

    if (disposing)

    {

      if (_UIControl != null)

      {

        _UIControl.Dispose();

        _UIControl = null;

      }

      GC.SuppressFinalize(this);

    }

  }

  finally

  {

    base.Dispose(disposing);

  }

}

 

public override IWin32Window Window

{

  get { return _UIControl; }

}

In the constructor we create the user control instance. The window pane and the user control hold unmanaged resources, so we use the Dispose pattern above. Overriding the Window property we can tell the window pane the user control instance to be hosted as the UI.

Handling changes in the editor content

When we modify any piece of information on the UI that is a part of the editor data, Visual Studio Shell should be notified about that change. This notification is used to keep track of changes in the document data. The shell uses it to administer the document data state in the Running Document Table an indicating the changed state with an “*” on the tab representing our open editor. Generally, when we change the UI it does not change the memory data footprint of the editor immediately. In our BlogItemEditor example we raise TextChanged events within the user control that raise a ContentChanged event. In the previous article I showed that the BlogItemEditorPane subscribes to the ContentChanged event of the user control and calls the OnContentChanged method of SimpleEditorPane to notify the Shell. So, let’s see how the pieces connect together to implement the change indication mechanism:

private bool _IsDirty;

 

protected virtual int OnIsDirty(out int pfIsDirty)

{

  pfIsDirty = _IsDirty ? 1 : 0;

  return VSConstants.S_OK;

}

 

int IPersistFileFormat.IsDirty(out int pfIsDirty)

{

  return OnIsDirty(out pfIsDirty);

}

 

int IVsPersistDocData.IsDocDataDirty(out int pfDirty)

{

  return ((IPersistFileFormat)this).IsDirty(out pfDirty);

}

 

protected virtual void OnContentChanged()

{

  if (!_Loading)

  {

    if (!_IsDirty)

    {

      if (!CanEditFile())

      {

        if (_UIControl.SupportsUndo) _UIControl.DoUndo();

        return;

      }

      _IsDirty = true;

    }

  }

}

Internally we use the _Dirty flag to indicate document changes. Through the implemented interfaces this flag is used as an integer value where 0 represents false, 1 represents true. Visual Studio periodically checks on open editor (at idle time) for “dirtiness”. Not the editor as a whole is checked but rather conceptual objects behind the editor. Since, in our implementation both IVsPersistDocData and IPersistFileFormat has a method (even with different names) to check this state. IVsPersistDocData calls the IsDocDataDirty method while IPersistFileFormat calls IsDirty. Both methods sign back “dirtiness” in an output parameter. To make it possible to create an alternative implementation in inherited classes, I declared the OnIsDirty virtual method and use it in both methods.

Before thinking that changes on user interface should simply set the _IsDirty to true, let’s have a look at them implementation of OnContentChanged. While we loading the document (_Loading indicates) the changes we made on data are initializing it so it is not going dirty. If the data is already dirty there is unnecessary to dirty it again. The CanEditFile checks if we are allowed to edit the data at all. If not, it tries to undo our changes. We set the _IsDirty to true only if we are allowed to edit the file. The CanEditFile method does a lot; it even asks the user if the files should be checked out from a source control store. Later I will show how it works, now, please believe it does its responsibility correctly.

Handling commands

Our editor pane implements the IOleCommandTarget interface and it provides the editor pane to process commands arriving from the shell. Processing commands actually means two different activities.

First, the environment will ask the editor pane about the status of commands. Examining the current context, our editor pane can declare that the particular command is supported or not. The shell will use this information to visually indicate this fact, and of course it will not route a non-supported commands to the editor pane for execution.

Second, when it is time to execute a command, the shell asks the editor pane to execute it.

As we saw in Part #14, each command is identified by a Guid, a so-called command set identifier and a uint value that is the command identifier within the logical group. Our methods receive these values and can check them to guess out the status of the commands or simply execute it.

In the implementation of SimpleEditorPane I provided some dispatch point through virtual methods to make it easy to hook into the command processing by derived classes:

protected virtual bool SupportsVSStd97Command(uint commandID, ref OLECMDF status)

{

  return false;

}

 

protected virtual bool ExecuteVSStd97Command(ExecArgs execArgs)

{

  return false; 

}

 

protected virtual bool SupportsOwnedCommand(QueryStatusArgs queryStatusArgs)

{

  return false; 

}

 

protected virtual bool ExecuteOwnedCommand(ExecArgs execArgs)

{

  return false;

}

 

protected virtual bool SupportsCommand(QueryStatusArgs queryStatusArgs)

{

  return false; 

}

 

protected virtual bool ExecuteCommand(ExecArgs execArgs)

{

  return false;

}

These virtual methods occur in pairs one method to check if the command is supported, its pair runs the command. Methods with VSStd97Command suffix process the standard commands coming from the environment. Actually, a few commands are handled separately: these are SelectAll, Cut, Copy, Paste, Redo and Undo. These commands are automatically forwarded to the UIControl representing the editor UI; that is why we expect the user control implement the ICommonCommandSupport interface.

Methods with OwnedCommand suffix allow processing commands having the command set GUID of our editor pane. These are actually the commands our editor defines. Methods with Command suffix process all other commands. Right now all methods return a false value indicating that commands are not supported and not executed. Let’s see how command processing happens.

Querying command status

The environment calls the QueryStatus method in idle time to refresh the command UI. It allows us to set the status of commands according to the current context. For example, when there is no selected text (or any other kind of selected object) in our editor, there is no reason to enable the Cut or Copy commands.

QueryStatus can be used to query only one command status or the status of more than one command in the same logical group:

public int QueryStatus(

  ref Guid pguidCmdGroup, // --- Command set GUID

  uint cCmds,             // --- Number of commands passed

  OLECMD[] prgCmds,       // --- Array for status of commands

  IntPtr pCmdText         // --- Used to dinamically change the command text

  )

{

  // --- Method body

}

Our method implementation uses the fact that only one command is passed to ask its status. In the body of the method we dispatch commands by their GUIDs and process them in logical groups. The method is quite straightforward; I highlighted the dispatch points where we call the virtual methods above:

public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds,

  IntPtr pCmdText)

{

  // --- Validate parameters

  if (prgCmds == null || cCmds != 1)

    return VSConstants.E_INVALIDARG;

 

  // --- Wrap parameters into argument type instance

  QueryStatusArgs statusArgs = new QueryStatusArgs(pguidCmdGroup);

  statusArgs.CommandCount = cCmds;

  statusArgs.Commands = prgCmds;

  statusArgs.PCmdText = pCmdText;

 

  // --- By default all commands are supported

  OLECMDF cmdf = OLECMDF.OLECMDF_SUPPORTED;

  if (pguidCmdGroup == VSConstants.GUID_VSStandardCommandSet97)

  {

    // --- Process standard Commands

    switch (prgCmds[0].cmdID)

    {

      case (uint)VSConstants.VSStd97CmdID.SelectAll:

        if (_UIControl.SupportsSelectAll) cmdf |= OLECMDF.OLECMDF_ENABLED;

        break;

      case (uint)VSConstants.VSStd97CmdID.Copy:

        if (_UIControl.SupportsCopy) cmdf |= OLECMDF.OLECMDF_ENABLED;

        break;

      // --- We process the other “common” commands in the same way

      // --- related code is omitted

      default:

        if (!SupportsVSStd97Command(prgCmds[0].cmdID, ref cmdf))

          return (int)(Constants.OLECMDERR_E_NOTSUPPORTED);

        break;

    }

    // --- Pass back the commmand support flag

    prgCmds[0].cmdf = (uint)cmdf;

    return VSConstants.S_OK;

  }

  // --- Check for commands owned by the editor

  else if (pguidCmdGroup == _CommandSetGuid)

  {

    return SupportsOwnedCommand(statusArgs)

      ? VSConstants.S_OK : (int)(Constants.OLECMDERR_E_NOTSUPPORTED);

      }

 

  // --- Check for any other commands

  return SupportsCommand(statusArgs)

    ? VSConstants.S_OK : (int)(Constants.OLECMDERR_E_NOTSUPPORTED);

}

At the beginning of the method body we wrap the QueryStatus arguments into a QueryStatusArgs instance and that instance is passed to the virtual dispatch methods. If we found a command supported, we enable them with the OLECMDF.OLECMDF_ENABLED flag.The method returns either the S_OK code if it supports the command and has set its status or OLECMDERR_E_NOTSUPPORTED if it does not know the command. In this case the shell tries to find some other command target that can handle the command.

Executing commands

We execute the command in a very similar way as their status is checked. This is the responsibility of the Exec method:

public int Exec(

  ref Guid pguidCmdGroup, // --- Command set GUID

  uint nCmdID,            // --- Command ID within the set

  uint nCmdexecopt,       // --- Optional execution arguments (not used here)

  IntPtr pvaIn,           // --- Pointer to optional input parameters (not used)

  IntPtr pvaOut           // --- Pointer to optional output parameters (not used)

  )

{

}

In the SimpleEditorPane implementation we use only the command identifiers (first two arguments):

public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt,

  IntPtr pvaIn, IntPtr pvaOut)

{

  // --- Wrap parameters into argument type instance

  ExecArgs execArgs = new ExecArgs(pguidCmdGroup, nCmdID);

  execArgs.CommandExecOpt = nCmdexecopt;

  execArgs.PvaIn = pvaIn;

  execArgs.PvaOut = pvaOut;

 

  if (pguidCmdGroup == VSConstants.GUID_VSStandardCommandSet97)

  {

    // --- Process standard Visual Studio Commands

    switch (nCmdID)

    {

      case (uint)VSConstants.VSStd97CmdID.Copy:

        _UIControl.DoCopy();

        return VSConstants.S_OK;

      case (uint)VSConstants.VSStd97CmdID.Cut:

        _UIControl.DoCut();

        return VSConstants.S_OK;

      // --- We process the other “common” commands in the same way

      // --- related code is omitted

      default:

        return ExecuteVSStd97Command(execArgs)

          ? VSConstants.S_OK : (int)(Constants.OLECMDERR_E_NOTSUPPORTED);

    }

  }

 

  // --- Execute commands owned by the editor

  else if (pguidCmdGroup == _CommandSetGuid)

  {

    return ExecuteOwnedCommand(execArgs)

      ? VSConstants.S_OK : (int)(Constants.OLECMDERR_E_NOTSUPPORTED);

  }

  // --- Execute any other command

  return ExecuteCommand(execArgs)

    ? VSConstants.S_OK : (int)(Constants.OLECMDERR_E_NOTSUPPORTED);

}

I have also highlighted the code where the virtual dispatch methods are called.

Persisting the editor files

Persisting data behind the editor can be logically split into two layers. The first (top) layer abstracts the save operation from its real physical implementation (does not care if the document is saved into a file or into a database). This layer is the responsibility of IVsPersistDocData. The second layer is the physical layer that actually saves the document data. In our case the editor data is saved into XML files and is the responsibility of IPersistFileFormat.

So, let us see how files go to the disk...

The idea behind IPersistFileFormat is that editors persisting data into files may support one or more file formats. For examples, images can be saved into several file formats. We can discover this behavior in the following methods of IPersistFileFormat:

int IPersistFileFormat.GetFormatList(out string ppszFormatList)

{

  return OnGetFormatList(out ppszFormatList);

}

 

int IPersistFileFormat.GetCurFile(out string ppszFilename, out uint pnFormatIndex)

{

  return OnGetCurFile(out ppszFilename, out pnFormatIndex);

}

 

int IPersistFileFormat.InitNew(uint nFormatIndex)

{

  return OnInitNew(nFormatIndex);

}

GetFormatList creates a list of format string used by the Save As dialog allowing the user to select the desired file format. The GetCurFile method is called to prepare the file name for the save operation. It is expected to retrieve the target file name and an index referring to the file format used. InitNew is called when it is time to prepare the file for the specified format. I created virtual methods to allow derived classes to override the default behavior. These virtual methods are quite simple, since we support only a single file format. OnGetFormatList returns a format string with multiple parts where each part is terminated with an EndLineChar. You can find much more details about the format in the comment of the method.

protected virtual int OnGetFormatList(out string ppszFormatList)

{

  string formatList =

    string.Format(CultureInfo.CurrentCulture,

    "Editor Files (*{0}){1}*{0}{1}{1}",

    FileExtensionUsed, EndLineChar);

  ppszFormatList = formatList;

  return VSConstants.S_OK;

}

 

protected virtual int OnGetCurFile(out string ppszFilename,

  out uint pnFormatIndex)

{

  // --- We only support 1 format so return its index

  pnFormatIndex = FileFormatIndex;

  ppszFilename = _FileName;

  return VSConstants.S_OK;

}

 

protected virtual int OnInitNew(uint nFormatIndex)

{

  if (nFormatIndex != FileFormatIndex)

  {

    throw new ArgumentException(Resources.ExceptionMessageFormat);

  }

  _IsDirty = false;

  return VSConstants.S_OK;

}

Saving the file is done by the Save method of IPersistFileFormat.

int IPersistFileFormat.Save(string pszFilename, int fRemember, uint nFormatIndex)

{

  // --- switch into the NoScribble mode

  _NoScribbleMode = true;

  try

  {

    // --- If file is null or same --> SAVE

    if (pszFilename == null || pszFilename == _FileName)

    {

      SaveFile(_FileName);

      _IsDirty = false;

    }

    else

    {

      // --- If remember --> SaveAs

      if (fRemember != 0)

      {

        _FileName = pszFilename;

        SaveFile(_FileName);

        _IsDirty = false;

      }

      else // --- Else, Save a Copy As

      {

        SaveFile(pszFilename);

      }

    }

  }

  finally

  {

    // --- Switch into the Normal mode

    _NoScribbleMode = false;

  }

  return VSConstants.S_OK;

}

We use the _NoScribbleMode field to sign that we are in save mode. The file can be saved in three modes:

—  When a null file name has been provided or we pass the name of the document, it is a Save operation.

—  If we provide a different file name and fRemember is not zero, it is a Save As operation.

—  If we provide a different file name and fRemember is zero, it is a Save As Copy operation.

In the first two cases we set the _IsDirty flag to false.

The physical save operation implemented by the abstract SaveFile method. The IPersistFileFormat provides a method to make cleanup and finalization work when save has been finished. Actually this method plays important role when we use multiple files in a transactional save, but it is another story. In our case the implementation is quite simple:

int IPersistFileFormat.SaveCompleted(string pszFilename)

{

  return OnSaveCompleted(pszFilename);

}

 

protected virtual int OnSaveCompleted(string pszFilename)

{

  return _NoScribbleMode ? VSConstants.S_FALSE : VSConstants.S_OK;

}

The Load operation’s logic is also simple:

int IPersistFileFormat.Load(string pszFilename, uint grfMode, int fReadOnly)

{

  // --- A valid file name is required.

  if ((pszFilename == null) && ((_FileName == null) || (_FileName.Length == 0)))

    throw new ArgumentNullException("pszFilename");

 

  _Loading = true;

  int hr = VSConstants.S_OK;

  try

  {

    // --- If the new file name is null, then this operation is a reload

    bool isReload = false;

    if (pszFilename == null)

    {

      isReload = true;

    }

    // --- Show the wait cursor while loading the file

    VsUIShell.SetWaitCursor();

    // --- Set the new file name

    if (!isReload)

    {

      _FileName = pszFilename;

    }

    // --- Load the file

    LoadFile(_FileName);

    _IsDirty = false;

    // --- Notify the load or reload

    NotifyDocChanged();

  }

  finally

  {

    _Loading = false;

  }

  return hr;

}

The method guards the document data getting dirty during the load operation with the _Loading flag. The method supports the reload operation (for example in case of external modifications). The physical operation is done by the abstract LoadFile method. When the file has been loaded into the memory, we set the _Dirty flag to false and administer notify the running document table through the NotifyDocChanged method.

Persisting the document data

IVsPersistDocData members are responsible for the upper layer of document management. Methods of this interface do not save or load the editor data directly, but rather trust on services of IPersistFileFormat. This layers main task is to organize flows related to the document data. There are three categories of flows handled by the IVsPersistDocData: loading (and reloading) data, saving data, renaming data (file). Let’s see each of them.

Loading the document data affects the following methods:

int IVsPersistDocData.OnRegisterDocData(uint docCookie, IVsHierarchy pHierNew,

  uint itemidNew)

{

  return VSConstants.S_OK;

}

 

int IVsPersistDocData.LoadDocData(string pszMkDocument)

{

  return ((IPersistFileFormat)this).Load(pszMkDocument, 0, 0);

}

 

int IVsPersistDocData.IsDocDataReloadable(out int pfReloadable)

{

  // --- Allow file to be reloaded

  pfReloadable = 1;

  return VSConstants.S_OK;

}

 

int IVsPersistDocData.ReloadDocData(uint grfFlags)

{

  return ((IPersistFileFormat)this).Load(null, grfFlags, 0);

}

When we open an editor first the OnRegisterDocData method is called. In this method we get a cookie for the RTD entry of the document, and the hierarchy information of the item opened. In this implementation we do not make any extra activity.

The next step is when the LoadDocData is called. This is the point where our document is loaded into the memory. You can see that we simply use the Load service of IPersistFileFormat. When the document is loaded the shell calls the IsDocDataReloadable method to allow us declaring whether the document is reloadable or not. We allow reloading. When the shell detects the file is changed outside of the IDE and the user confirms reload, the ReloadDocData method is called that also uses IPersistFileFormat.Load. However, in our case reloading the editor file is just a theoretical opportunity, since our editor does not implement the IVsDocDataFileChangeControl interface.

Saving the document simply causes the SaveDocData method to be fired. The method has the following signature:

int IVsPersistDocData.SaveDocData(VSSAVEFLAGS dwSave,

  out string  pbstrMkDocumentNew, out int pfSaveCanceled) { ... }

This method gets two arguments besides the name of the file (pbstrMkDocumentNew). The dwSave parameter is a set of flags describing how the save action has been initiated:

—  VSSAVE_Save: The file is to be saved on itself.

—  VSSAVE_SaveAs: The user has to be prompted for a filename and the file should be saved to the specified name.

—  VSSAVE_SaveCopyAs: The user has to be prompted for a filename and the file should be saved as a copy to the specified name.

—  VSSAVE_SaveSilent: The file should be saved without any prompt or confirmation. This is used when for example a code generation utility modifies a file in the background.

The pfSaveCancelled flag returns non-zero value if the save process has been cancelled (for example the user canceled the Save As dialog.)

The behavior of the SaveDocData method is different depending on the dwSave flag:

int IVsPersistDocData.SaveDocData(VSSAVEFLAGS dwSave,

  out string pbstrMkDocumentNew, out int pfSaveCanceled)

{

  pbstrMkDocumentNew = null;

  pfSaveCanceled = 0;

  int hr;

 

  switch (dwSave)

  {

    case VSSAVEFLAGS.VSSAVE_Save:

    case VSSAVEFLAGS.VSSAVE_SilentSave:

    {

      IVsQueryEditQuerySave2 queryEditQuerySave =

        (IVsQueryEditQuerySave2)GetService(typeof(SVsQueryEditQuerySave));

      // --- Call QueryEditQuerySave

      uint result;

      hr = queryEditQuerySave.QuerySaveFile(

        _FileName,    // --- filename

        0,            // --- flags

        null,         // --- file attributes

        out result);  // --- result

 

      if (ErrorHandler.Failed(hr)) return hr;

 

      // --- Process according to result from QuerySave

      switch ((tagVSQuerySaveResult)result)

      {

        case tagVSQuerySaveResult.QSR_NoSave_Cancel:

        // --- Note that this is also case tagVSQuerySaveResult.

        // --- QSR_NoSave_UserCanceled because these two tags have the same value.

        pfSaveCanceled = ~0;

        break;

 

        case tagVSQuerySaveResult.QSR_SaveOK:

        {

          // --- Call the shell to do the save for us

          hr = VsUIShell.SaveDocDataToFile(dwSave, this, _FileName,

            out pbstrMkDocumentNew, out pfSaveCanceled);

          if (ErrorHandler.Failed(hr)) return hr;

        }

        break;

 

        case tagVSQuerySaveResult.QSR_ForceSaveAs:

        {

          // --- Call the shell to do the SaveAS for us

          hr = VsUIShell.SaveDocDataToFile(VSSAVEFLAGS.VSSAVE_SaveAs, this,

            _FileName, out pbstrMkDocumentNew, out pfSaveCanceled);

          if (ErrorHandler.Failed(hr)) return hr;

        }

        break;

 

        case tagVSQuerySaveResult.QSR_NoSave_Continue:

        // --- In this case there is nothing to do.

        break;

 

        default:

          throw new COMException(Resources.ExceptionMessageQEQS);

      }

      break;

    }

 

    case VSSAVEFLAGS.VSSAVE_SaveAs:

    case VSSAVEFLAGS.VSSAVE_SaveCopyAs:

    {

      // --- Make sure the file name as the right extension

      if (String.Compare(FileExtensionUsed, Path.GetExtension(_FileName), true,

        CultureInfo.CurrentCulture) != 0)

      {

        _FileName += FileExtensionUsed;

      }

      // --- Call the shell to do the save for us

      hr = VsUIShell.SaveDocDataToFile(dwSave, this, _FileName,

        out pbstrMkDocumentNew, out pfSaveCanceled);

      if (ErrorHandler.Failed(hr)) return hr;

      break;

    }

    default:

      throw new ArgumentException(Resources.ExceptionMessageSaveFlag);

  }

  return VSConstants.S_OK;

}

This method has a complex logic inside. Before telling what this logic does I would like to highlight a few point of interest. The QuerySaveFile method of the IVsQueryEditQuerySave2 interface notifies the shell that a file is about to be saved. The method returns a value—that is later casted to tagVSQuerySaveResult—determining how to save the file. The SaveDocDataToFile method of the IVsUIShell interface is a helper method that implements the Save As dialog and initiates the physical save. We can pass flags to this method to tell how to display the dialog box (if to display at all). In my implementation you do not find the IVsUIShell interface since I refactored the behavior into a static class called VsUIShell.

So, let’s see what SaveDocData does! If we call it with VSSAVE_SaveAs or VSSAVE_SaveAsCopy flag, it provides a Save As dialog for the user to specify the file name where the editor content should be saved.

If we call it with VSSAVE_SAVE or VSSAVE_Silent, the behavior is a bit more complex. By calling the QuerySaveFile method we notify the shell about the save operation. The method answers with a result value that determines how to continue with the save. This value is one of the tagVSQuerySaveResult enumeration values.

In case of QSR_NoSave_Cancel the save operation has been cancelled, sonly thing we should do is to set the pfSaveCancelled flag to a non-zero value. QSR_NoSave_Continue indicates that no save should be done and the operation can continue on its normal way. This is the case for example when we try to save an unchanged document.

In case of QSR_SaveOk we simply save the document with its provided name. Should we got the QSR_SaveForceSaveAs result, we must use the Save As mode. One example of this is when our file is first saved and it has no explicit name. Another example can be when our original file is read-only on the disk and we do not want to override it.

Some other details...

Even going on so many parts of the code, there are a few details not treated yet. Here I deal with to of them I have found the most important.

Integration with source control handling

Our editor has a single point when it should cooperate with the current source control system (SCC)—if there is any—used in Visual Studio. That is the moment when we try to edit a document. That document can be a “checked-in” state in the SCC so it is taken into account as read-only. To be able to edit, first we have to check it out from the source control. This functionality is handled by the CanEditFile method that is called when we try to set the document dirty. The mechanism behind the method does a lot of work:

private bool CanEditFile()

{

  // --- Check the status of the recursion guard

  if (_GettingCheckoutStatus) return false;

 

  try

  {

    _GettingCheckoutStatus = true;

 

    IVsQueryEditQuerySave2 queryEditQuerySave =

      (IVsQueryEditQuerySave2)GetService(typeof(SVsQueryEditQuerySave));

 

    // ---Now call the QueryEdit method to find the edit status of this file

    string[] documents = { _FileName };

    uint result;

    uint outFlags;

 

    int hr = queryEditQuerySave.QueryEditFiles(

      0, // Flags

      1, // Number of elements in the array

      documents, // Files to edit

      null, // Input flags

      null, // Input array of VSQEQS_FILE_ATTRIBUTE_DATA

      out result, // result of the checkout

      out outFlags // Additional flags

      );

    if (ErrorHandler.Succeeded(hr) && (result ==

      (uint)tagVSQueryEditResult.QER_EditOK))

    {

      return true;

    }

  }

  finally

  {

    _GettingCheckoutStatus = false;

  }

  return false;

}

Calling the QueryEditFiles method within the body pops up a query dialog. While the dialog is waiting for the user, the shell automatically can call the method checking for dirtiness and that will call CanEditFile again. To avoid recursion we use the _GettingCheckOutStatus flag as a guard with the necessary try...finally block. We use the IVsQueryEditQuerySave2 interface to check the state of our file. Without going into subtle details how this method works, let me tell you only a few hints. If necessary, this method asks the user to check out the file from the SCC and even checks it out. Returning a QER_EditOk status tells that we can edit the file.

Integration with RDT

In Part #15 I introduced you the Running Document Table that is the infrastructure element in Visual Studio to keep track of open documents. Visual Studio itself does not know anything about open documents; it is the responsibility of the editor to notify RDT.

In our example we notify the RDT when our document is loaded into the memory. It is done with the NotifyDocChanged private method that is called by IPersistFileFormat.Load:

private void NotifyDocChanged()

{

  // --- Make sure that we have a file name

  if (_FileName.Length == 0) return;

 

  // --- Get a reference to the Running Document Table

  IVsRunningDocumentTable runningDocTable =

    (IVsRunningDocumentTable)GetService(typeof(SVsRunningDocumentTable));

 

  // --- Lock the document

  uint docCookie;

  IVsHierarchy hierarchy;

  uint itemID;

  IntPtr docData;

  int hr = runningDocTable.FindAndLockDocument(

    (uint)_VSRDTFLAGS.RDT_ReadLock,

    _FileName,

    out hierarchy,

    out itemID,

    out docData,

    out docCookie);

  ErrorHandler.ThrowOnFailure(hr);

 

  // --- Send the notification

  hr = runningDocTable.NotifyDocumentChanged(docCookie,

    (uint)__VSRDTATTRIB.RDTA_DocDataReloaded);

 

  // --- Unlock the document.

  runningDocTable.UnlockDocument((uint)_VSRDTFLAGS.RDT_ReadLock, docCookie);

  ErrorHandler.ThrowOnFailure(hr);

}

We access the RDT through the IVsRunningDocumentTable service. To do any operation with the document we have to find it in the RDT by its file name (or “moniker”). It is done with the FindAndLockDocument method that passes back a cookie (docCookie) to refer later to the RDT entry. With the NotifyDocumentChanged method we tell the RDT the event about an entry change. Here we pass the cookie and an attribute (RDTA_DocDataReloaded) to tell what kind of change we report. Since we locked the document, we must call UnlockDocument to release the lock held.

Future development opportunities

I would not say SimpleEditorPane<,> is a complex class, I’d rather tell you it has quite long code. It seems complete from a certain perspective, however there are many imaginations in my mind how to make it smoother, better.

Right now, SimpleEditorPane has only two generic parameters: one for the editor factory class, one for the custom control implementing the UI. You might feel—as I feel—that it is possible to make the memory data footprint type (BlogItemEditorData is my example) also a generic type parameter. To do that, we have to enumerate the responsibilities of the memory data footprint type to make it really generic.

Other opportunities are to implement other interfaces adding behavior to custom editors. When you create your custom editor with the VSPackage wizard, it will implement a dozen more interfaces.

I am thinking of going toward the way I have just started and adding more “stereotype” behavior to the SimpleEditorPane class or to its inheritors. Till that time I leave it to you to use and change it. If you have recommendations, or you made changes, extensions on SimpleEditorPane, please let me know!

Where we are?

We are at the end of a three-part article series about custom editors. You could see that the architecture behind custom editors is quite simple but the implementation is really complex. This is due to the fact that the elements of the architecture are abstract elements. Custom editors have four separate architectural components:

—  Editor factory responsible to create editor instances.

—  Document view representing a visual manifestation of a custom editor. One editor can have one or more view.

—  Document data representing the information behind the custom editor (independently of views visualizing the data).

—  Editor pane that is responsible to provide windowing services for the editor and provide the role of a controller among document data and its views.

Document data is an abstraction of really any kind of data in memory and any kind of persistence method related to that. Document data can be saved in one or more files but also one or more document can go in the same file. Not files are the only way of persistence but also databases or actually anything.

We can create views to our editor the way we prefer or guess is the best for an editor, technologies used are our decision. We have several user interface styles for an editor (see Part #15), so our possibilities are not really constrained.

Having this incredible flexibility it is understandable why implementing a custom editor requires so long code. We are not obliged to always write the code from scratch. Identifying stereotype behavior (what I did in case of BlogItemEditor) allows us to factor out large amount of reusable code related to custom editors.

We have only scratched the surface. There are thousand things to move on with custom editors. Welcome at depth of 40 meters. Now, it is time to ascend...


Posted Mar 17 2008, 08:02 AM by inovak
Filed under:

Comments

GA30 wrote re: LearnVSXNow! #17 - Creating a simple custom editor — under pressure
on Sun, Jun 28 2009 23:56

Inovak,

I am using VSExtra as the framework for a custom editor I am working on. I have a question for you regarding EditorPaneBase. In here you are storing the path to the editor file in the _FileName field. But I see this field is not being updated in IVsPersistDocData.RenameDocData when the file is renamed. So when I save the file after having renamed it the save is saved to its old file name. I changed this method and am now updating the _FileName field to the new file name resulting from the rename operation. Is there a reason you are not updating this field?

Thank you for your knowledge (and publishing of it) in the VSX area! I'm new to VSX and I've gotta say it's probably the more challending type of development I've faced recently more so because of the lack of documentation in this area relative to other areas.

// --------------------------------------------------------------------------------

   /// <summary>

   /// Renames the document data.

   /// </summary>

   /// <param name="grfAttribs">

   /// File attribute of the document data to be renamed. See the data type

   /// __VSRDTATTRIB.

   /// </param>

   /// <param name="pHierNew">

   /// Pointer to the IVsHierarchy interface of the document being renamed.

   /// </param>

   /// <param name="itemidNew">

   /// Item identifier of the document being renamed. See the data type VSITEMID.

   /// </param>

   /// <param name="pszMkDocumentNew">Path to the document being renamed.</param>

   /// <returns>S_OK if the method succeeds.</returns>

   // --------------------------------------------------------------------------------

   int IVsPersistDocData.RenameDocData(uint grfAttribs, IVsHierarchy pHierNew,

     uint itemidNew, string pszMkDocumentNew)

   {

     return VSConstants.S_OK;

   }

inovak wrote re: LearnVSXNow! #17 - Creating a simple custom editor — under pressure
on Mon, Jun 29 2009 8:32

Hey GA30,

What you have found is rather a bug (or feature  :-) than intention. Thanks for finding it. If you would try to modify the VSXtra source and correct this bug and check it, I would be very happy. If you have the modifications done, you can upload them to CodePlex as a patch.

Visual guard for .Net wrote re: LearnVSXNow! #17 - Creating a simple custom editor — under pressure
on Wed, Mar 23 2011 14:37

Thanks for sharing this information.Appreciated!

Mark wrote re: LearnVSXNow! #17 - Creating a simple custom editor — under pressure
on Wed, Aug 15 2012 19:47

It article looks the last for 'Custom Editor' please Can you share the code?