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