Solving Undo/Redo Problems with the MSHTML Editor






4.76/5 (9 votes)
This article presents solutions to problems relating to Undo/Redo when using the MSHTML editor.
Introduction
Recently, I made the fateful decision to add a simple HTML editor to an application. I found some code that uses the MSHTML control to achieve this and began to work to make it work right. This article presents a few of the problems I encountered related to Undo/Redo, and the solutions I found with the hope it can save some time for others.
Background
MSHTML is a Microsoft library that provides very powerful capabilities for creating and editing HTML documents. An introduction to the use of it as an editor can be found here. You can find quite a bit of sample code online that demonstrates how to create an HTML editor using this component. For example, see this article by Carl Nolan and this archived code sample from Nikhil Kothari.
While I found the available references on the web very useful, they didn't solve some problems related to Undo and Redo. In particular,
- I needed to be able to group compound operations into a single Undo/Redo unit, and
- I needed to be able to clear the Undo buffer, so that a user couldn't undo the initial assignment of raw HTML to the editor control.
This article outlines the solutions I found for these problems.
Grouping Compound Operations
The granularity of the operations you can perform in code is sometimes smaller than the user initiated action. For example, when you are adding a column to a table, you need to iterate through each row in the table, inserting a cell in the same location in each row, like this:
// find the existing row the user is on and perform the insertion
int index = cell.cellIndex;
foreach (mshtml.IHTMLTableRow row in table.rows)
{
row.insertCell(index);
}
Although the user has requested a single operation (insert a column), the MSHTML interface treats each programatic insertion as an operation, so the user would have to hit ^Z table.rows.count times to undo the operation.
The solution I found is to use the IMarkupServices interface provided by Microsoft like this:
var markupServices = (mshtml.IMarkupServices)document;
markupServices.BeginUndoUnit(0);
...
//Multiple operations
...
markupServices.EndUndoUnit();
This groups all operations into a single undo/redo unit.
Clearing the Undo Buffer
If you initialize your MSHTML control with some HTML content, that assignment will be part of the undo buffer, so, unless you do something about it, the end user can undo the assignment with ^Z (or clicking an undo control.) Avoiding this turned out to be a bit more painful than I expected. If you find an easier way, please help us all out by posting it.
To clear the Undo buffer, you need to gain access to the IOleUndoManager for the MSHTML control. Thanks to Jon David for his post here for most of the code. First, you need to define the IOleUndoManager interface as follows:
/// <summary>
/// These interface definitions are required for obtaining the IOldUndoManager, which
/// is needed for clearing the undo buffer.
/// </summary>
[ComVisible(true),
ComImport(),
Guid("6d5140c1-7436-11ce-8034-00aa006009fa"),
InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
public interface UCOMIServiceProvider
{
IntPtr QueryService(ref Guid guidService, ref Guid riid);
}
public interface IOleParentUndoUnit { }
public interface IOleUndoUnit { }
public interface IEnumOleUndoUnits { }
[ComVisible(true), ComImport(), Guid("D001F200-EF97-11CE-9BC9-00AA00608E01"),
InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
public interface IOleUndoManager
{
void Open(IOleParentUndoUnit ParentUndoUnit);
void Close(IOleParentUndoUnit ParentUndoUnit, bool Commit);
void Add(IOleUndoUnit UndoUnit);
void GetOpenParentState(ref int State);
void DiscardFrom(IOleUndoUnit UndoUnit);
void UndoTo(IOleUndoUnit UndoUnit);
void RedoTo(IOleUndoUnit UndoUnit);
void EnumUndoable(ref IEnumOleUndoUnits ppEnum);
void EnumRedoable(ref IEnumOleUndoUnits ppEnum);
void GetLastUndoDescription(ref string Description);
void GetLastRedoDescription(ref string Description);
void Enable(bool Enable);
}
Now define the Guids you'll use for identifying it as follows in your code:
private Guid SID_SOleUndoManager = new Guid("D001F200-EF97-11CE-9BC9-00AA00608E01");
private Guid IID_IOleUndoManager = new Guid("D001F200-EF97-11CE-9BC9-00AA00608E01");
Write a function that returns the UndoManager:
private IOleUndoManager GetUndoManager()
{
IOleUndoManager oUndoManager;
UCOMIServiceProvider isp =(UCOMIServiceProvider)document;
IntPtr ip = isp.QueryService( ref SID_SOleUndoManager, ref IID_IOleUndoManager);
oUndoManager = (IOleUndoManager)Marshal.GetObjectForIUnknown(ip);
return oUndoManager;
}
And use it as follows to clear the buffer:
/// <summary>
/// Clear the undo buffer so that the user can't accidentally "undo"
/// the assignment of the HTML to the control
/// </summary>
public void ClearUndoBuffer()
{
IOleUndoManager oUndoMgr = GetUndoManager();
oUndoMgr.Enable(false);
oUndoMgr.Enable(true);
}
UPDATE: A comment below says that the enabling and disabling of the undo manager shown above does not work in some circumstances. He found that replacing:
oUndoMgr.Enable(false);
oUndoMgr.Enable(true);
with
oUndoMgr.DiscardFrom(null);
in the ClearUndoBuffer() method solved the problem for him.
Points of Interest
I hope this is of some help to you if you're working with the MSHTML control. The intent of this article is to share some of the research I did to find these solutions and group them together in a place others can reference them. Much of what is here came initially from the referenced authors.
History
June 4, 2013 - Initial Version
June 10, 2014 - Added alternative way of clearing the undo/redo buffer based on comment below.