Friday, July 10, 2009

A WebBrowser Implementation That Actually Raises Events!

A question that comes up rather frequently on the Forums is "Why doesn't the WebBrowser event ___________ ever get raised?" Typically, the blank is filled in with "MouseClick" and "KeyDown" and other input events. The reason for this is the fact that the WebBrowser control is an ActiveX-based control. Essentially, all of the events that would normally be raised by the control itself, are instead captured inside the document that resides within the control. In order to get the events to be raised at the control level, it's important to attach the event handlers to the document, and not the control. Unfortunately, the default implementation of WebBrowser doesn't expose the document's events at this level, so you're left with no way of tracking what's happening within the WebBrowser control, as far as input is concerned. Well, no more. Here's an implementation of WebBrowser, called ActiveWebBrowser, that can handle these events, and exposes them publicly at the WebBrowser level, without interfering with the existing events on the page.

This code will require you to reference the COM library called "Microsoft HTML Object Library" in your code, which should be available through the COM tab in the Add References dialog.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.Runtime.InteropServices.ComTypes;
using mshtml;


namespace WindowsFormsApplication1
{
public class MSHtmlEventArgs : EventArgs
{
public MSHtmlEventArgs(mshtml.IHTMLEventObj obj)
{
EventObj = obj;
}

public mshtml.IHTMLEventObj EventObj { get; private set; }
}


public class ActiveWebBrowser : WebBrowser
{
private IConnectionPoint icp;
private int cookie = -1;

public event EventHandler<MSHtmlEventArgs> DocumentActivate;
public event EventHandler<MSHtmlEventArgs> DocumentAfterUpdate;
public event EventHandler<MSHtmlEventArgs> DocumentBeforeActivate;
public event EventHandler<MSHtmlEventArgs> DocumentBeforeDeactivate;
public event EventHandler<MSHtmlEventArgs> DocumentBeforeEditFocus;
public event EventHandler<MSHtmlEventArgs> DocumentBeforeUpdate;
public event EventHandler<MSHtmlEventArgs> DocumentCellChange;
public event EventHandler<MSHtmlEventArgs> DocumentClick;
public event EventHandler<MSHtmlEventArgs> DocumentContextMenu;
public event EventHandler<MSHtmlEventArgs> DocumentControlSelect;
public event EventHandler<MSHtmlEventArgs> DocumentDataAvailable;
public event EventHandler<MSHtmlEventArgs> DocumentDataSetChanged;
public event EventHandler<MSHtmlEventArgs> DocumentDataSetComplete;
public event EventHandler<MSHtmlEventArgs> DocumentDoubleClick;
public event EventHandler<MSHtmlEventArgs> DocumentDeactivate;
public event EventHandler<MSHtmlEventArgs> DocumentDragStart;
public event EventHandler<MSHtmlEventArgs> DocumentErrorUpdate;
public event EventHandler<MSHtmlEventArgs> DocumentFocusIn;
public event EventHandler<MSHtmlEventArgs> DocumentFocusOut;
public event EventHandler<MSHtmlEventArgs> DocumentHelp;
public event EventHandler<MSHtmlEventArgs> DocumentKeyDown;
public event EventHandler<MSHtmlEventArgs> DocumentKeyPress;
public event EventHandler<MSHtmlEventArgs> DocumentKeyUp;
public event EventHandler<MSHtmlEventArgs> DocumentMouseDown;
public event EventHandler<MSHtmlEventArgs> DocumentMouseMove;
public event EventHandler<MSHtmlEventArgs> DocumentMouseUp;
public event EventHandler<MSHtmlEventArgs> DocumentMouseOut;
public event EventHandler<MSHtmlEventArgs> DocumentMouseOver;
public event EventHandler<MSHtmlEventArgs> DocumentMouseWheel;
public event EventHandler<MSHtmlEventArgs> DocumentPropertyChange;
public event EventHandler<MSHtmlEventArgs> DocumentReadyStateChange;
public event EventHandler<MSHtmlEventArgs> DocumentRowEnter;
public event EventHandler<MSHtmlEventArgs> DocumentRowExit;
public event EventHandler<MSHtmlEventArgs> DocumentRowsDelete;
public event EventHandler<MSHtmlEventArgs> DocumentRowsInserted;
public event EventHandler<MSHtmlEventArgs> DocumentSelectionChange;
public event EventHandler<MSHtmlEventArgs> DocumentSelectStart;
public event EventHandler<MSHtmlEventArgs> DocumentStop;

protected override void OnDocumentCompleted(WebBrowserDocumentCompletedEventArgs e)
{
base.OnDocumentCompleted(e);

IConnectionPointContainer icpc;
icpc = (IConnectionPointContainer)this.Document.DomDocument;
Guid guid = typeof(HTMLDocumentEvents2).GUID;
icpc.FindConnectionPoint(ref guid, out icp);
icp.Advise(new HandleWebBrowserDHTMLEvents(this), out cookie);
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
if (-1 != cookie) icp.Unadvise(cookie);
cookie = -1;
}
base.Dispose(disposing);
}

class HandleWebBrowserDHTMLEvents : mshtml.HTMLDocumentEvents2
{
private ActiveWebBrowser _webBrowser;

public HandleWebBrowserDHTMLEvents(ActiveWebBrowser webBrowser)
{
_webBrowser = webBrowser;
}

public void onactivate(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentActivate != null)
_webBrowser.DocumentActivate(_webBrowser, new MSHtmlEventArgs(e));
}

public void onafterupdate(mshtml.IHTMLEventObj e) {
if (_webBrowser.DocumentAfterUpdate != null)
_webBrowser.DocumentAfterUpdate(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onbeforeactivate(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentBeforeActivate != null)
_webBrowser.DocumentBeforeActivate(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public bool onbeforedeactivate(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentBeforeDeactivate != null)
_webBrowser.DocumentBeforeDeactivate(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void onbeforeeditfocus(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentBeforeEditFocus != null)
_webBrowser.DocumentBeforeEditFocus(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onbeforeupdate(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentBeforeUpdate != null)
_webBrowser.DocumentBeforeUpdate(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void oncellchange(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentCellChange != null)
_webBrowser.DocumentCellChange(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onclick(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentClick != null)
_webBrowser.DocumentClick(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public bool oncontextmenu(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentContextMenu != null)
_webBrowser.DocumentContextMenu(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public bool oncontrolselect(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentControlSelect != null)
_webBrowser.DocumentControlSelect(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void ondataavailable(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentDataAvailable != null)
_webBrowser.DocumentDataAvailable(_webBrowser, new MSHtmlEventArgs(e));
}

public void ondatasetchanged(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentDataSetChanged != null)
_webBrowser.DocumentDataSetChanged(_webBrowser, new MSHtmlEventArgs(e));
}

public void ondatasetcomplete(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentDataSetComplete != null)
_webBrowser.DocumentDataSetComplete(_webBrowser, new MSHtmlEventArgs(e));
}

public bool ondblclick(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentDoubleClick != null)
_webBrowser.DocumentDoubleClick(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void ondeactivate(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentDeactivate != null)
_webBrowser.DocumentDeactivate(_webBrowser, new MSHtmlEventArgs(e));
}

public bool ondragstart(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentDragStart != null)
_webBrowser.DocumentDragStart(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public bool onerrorupdate(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentErrorUpdate != null)
_webBrowser.DocumentErrorUpdate(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void onfocusin(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentFocusIn != null)
_webBrowser.DocumentFocusIn(_webBrowser, new MSHtmlEventArgs(e));
}

public void onfocusout(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentFocusOut != null)
_webBrowser.DocumentFocusOut(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onhelp(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentHelp != null)
_webBrowser.DocumentHelp(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void onkeydown(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentKeyDown != null)
_webBrowser.DocumentKeyDown(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onkeypress(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentKeyPress != null)
_webBrowser.DocumentKeyPress(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void onkeyup(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentKeyUp != null)
_webBrowser.DocumentKeyUp(_webBrowser, new MSHtmlEventArgs(e));
}

public void onmousedown(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentMouseDown != null)
_webBrowser.DocumentMouseDown(_webBrowser, new MSHtmlEventArgs(e));
}

public void onmousemove(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentMouseMove != null)
_webBrowser.DocumentMouseMove(_webBrowser, new MSHtmlEventArgs(e));
}

public void onmouseout(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentMouseOut != null)
_webBrowser.DocumentMouseOut(_webBrowser, new MSHtmlEventArgs(e));
}

public void onmouseover(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentMouseOver != null)
_webBrowser.DocumentMouseOver(_webBrowser, new MSHtmlEventArgs(e));
}

public void onmouseup(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentMouseUp != null)
_webBrowser.DocumentMouseUp(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onmousewheel(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentMouseWheel != null)
_webBrowser.DocumentMouseWheel(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void onpropertychange(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentPropertyChange != null)
_webBrowser.DocumentPropertyChange(_webBrowser, new MSHtmlEventArgs(e));
}

public void onreadystatechange(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentReadyStateChange != null)
_webBrowser.DocumentReadyStateChange(_webBrowser, new MSHtmlEventArgs(e));
}

public void onrowenter(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentRowEnter != null)
_webBrowser.DocumentRowEnter(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onrowexit(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentRowExit != null)
_webBrowser.DocumentRowExit(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void onrowsdelete(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentRowsDelete != null)
_webBrowser.DocumentRowsDelete(_webBrowser, new MSHtmlEventArgs(e));
}

public void onrowsinserted(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentRowsInserted != null)
_webBrowser.DocumentRowsInserted(_webBrowser, new MSHtmlEventArgs(e));
}

public void onselectionchange(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentSelectionChange != null)
_webBrowser.DocumentSelectionChange(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onselectstart(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentSelectStart != null)
_webBrowser.DocumentSelectStart(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public bool onstop(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentStop != null)
_webBrowser.DocumentStop(_webBrowser, new MSHtmlEventArgs(e));
return true;
}
}
}
}

Thursday, July 9, 2009

AccessViolationException and TextBox AutoComplete

The other day, an interesting question came up on the Windows Forms forum regarding updating the data items of a TextBox that is bound to AutoComplete. The original poster was looking for a way to update the items in the autocomplete contextually, based on what the user's first few entries are, not unlike the autocomplete that is so helpful on Google. In trying to help the user, I created a windows form, added a text box, and handled the TextChanged event. In the handler, I edited the items that were located within the AutoCompleteStringCollection accessible via the AutoCompleteCustomSource property of the TextBox. Then I ran into an issue.

Apparently, setting this property at runtime, while the AutoCompleteCustomSource is being used by the TextBox, can cause frequent AccessViolationExceptions, which were thrown at the level of the call to Application.Run in the Main method.

This is bad.

So I did a bit of searching, and managed to find a workaround, written by Sheng Jiang on his blog. I ported this code over to C#, and it seems to be working well, so I thought I would post it here for anybody else who runs into the issue. I tested this code for a while, and it seems to have solved the issue.

First, add the following code to a clean code file in your application:


using WindowsFormsApplication1;
using System;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.InteropServices;
using System.Security;
using System.Windows.Forms;
using System.ComponentModel;

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("EAC04BC0-3791-11d2-BB95-0060977B464C")]
[SuppressUnmanagedCodeSecurity]
public interface IAutoComplete2
{
int Init([In] HandleRef hwndEdit, [In] IEnumString punkACL, [In] string pwszRegKeyPath, [In] string pwszQuickComplete);
void Enable([In] bool fEnable);
int SetOptions([In] int dwFlag);
void GetOptions([Out] IntPtr pdwFlag);

}


internal class CustomSource : BindingSource, IEnumString
{
// Fields
private static Guid autoCompleteClsid = new Guid("{00BB2763-6A77-11D0-A535-00C04FD7D062}");
private IAutoComplete2 autoCompleteObject2;
private int current;
private int size;
private string[] strings;

public void Bind(TextBox textBox)
{
this.autoCompleteObject2.SetOptions((int)textBox.AutoCompleteMode);
this.autoCompleteObject2.Init(new HandleRef(textBox, textBox.Handle),
this, string.Empty, string.Empty);
}

// Methods
public CustomSource(string[] strings)
{
Array.Clear(strings, 0, this.size);
if (strings != null)
{
this.strings = strings;
}
this.current = 0;
this.size = (strings == null) ? 0 : strings.Length;
Guid gUID = typeof(IAutoComplete2).GUID;
object obj2 = Activator.CreateInstance(Type.GetTypeFromCLSID(autoCompleteClsid));
this.autoCompleteObject2 = (IAutoComplete2)obj2;
}

public bool Bind(HandleRef edit, int options)
{
bool flag = false;
if (this.autoCompleteObject2 == null)
{
return flag;
}
try
{
this.autoCompleteObject2.SetOptions(options);
this.autoCompleteObject2.Init(edit, this, null, null);
return true;
}
catch
{
return false;
}
}

public void RefreshList(string[] newSource)
{
Array.Clear(this.strings, 0, this.size);
if (this.strings != null)
{
this.strings = newSource;
}
this.current = 0;
this.size = (this.strings == null) ? 0 : this.strings.Length;
}

public void ReleaseAutoComplete()
{
if (this.autoCompleteObject2 != null)
{
Marshal.ReleaseComObject(this.autoCompleteObject2);
this.autoCompleteObject2 = null;
}
}

void IEnumString.Clone(out IEnumString ppenum)
{
ppenum = new CustomSource(this.strings);
}

public string DisplayMember { get; set; }

int IEnumString.Next(int celt, string[] rgelt, IntPtr pceltFetched)
{
if (celt < 0)
{
return -2147024809;
}

int index = 0;

while ((this.current < this.size) && (celt > 0))
{
object item = this.strings[this.current];
bool useDisplayMember = false;
if (string.IsNullOrEmpty(DisplayMember))
{
ICustomTypeDescriptor customTypeDescriptor = item as CustomTypeDescriptor;
if (customTypeDescriptor != null)
{
PropertyDescriptorCollection descriptorCollection =
customTypeDescriptor.GetProperties();
if (descriptorCollection != null)
{
PropertyDescriptor propertyDescriptor = descriptorCollection[DisplayMember];
if (propertyDescriptor != null)
{
rgelt[index] = propertyDescriptor.GetValue(item).ToString();
useDisplayMember = true;
}
}
}

if (!useDisplayMember)
{
if (item != null)
rgelt[index] = item.ToString();
}
}

current++;
index++;
celt--;
}

if (pceltFetched != IntPtr.Zero)
{
Marshal.WriteInt32(pceltFetched, index);
}
if (celt != 0)
{
return 1;
}

return 0;
}

void IEnumString.Reset()
{
this.current = 0;
}

int IEnumString.Skip(int celt)
{
this.current += celt;
if (this.current >= this.size)
{
return 1;
}
return 0;
}
}


And then, here's the key.... After instantiating the CustomSource, call the "Bind" method, passing in the target TextBox. When you wish to change the items in the TextBox's autocomplete, simply create a new one, then call "ReleaseAutoComplete()" on the old one, and call the Bind method on the new one, again, passing in the TextBox. Below is some sample code that demonstrates this functionality.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security;
using System.Runtime.InteropServices.ComTypes;

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
// here are my suggestions. You can do whatever you want here.
string[] suggestions = { "Dog", "Cat", "Box" };

string lastText = string.Empty;
static bool textChanging = false;
private CustomSource autoComplete;

public Form1()
{
InitializeComponent();
}

protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);

autoComplete = new CustomSource(suggestions);
textBox1.AutoCompleteMode = AutoCompleteMode.Suggest;
textBox1.AutoCompleteSource = AutoCompleteSource.CustomSource;
autoComplete.Bind(textBox1);
}

private void textBox1_TextChanged(object sender, EventArgs e)
{
if (!textChanging)
{
textChanging = true;
string prefix = "";
string text = textBox1.Text;
bool changed = false;

if (lastText.Length < text.Length && text.EndsWith(" "))
{
prefix = text;
changed = true;
}
else if (lastText.Length < text.Length && lastText.EndsWith(" ") && text.Contains(' '))
{
prefix = text.Substring(0, text.LastIndexOf(' '));
changed = true;
}

if (changed)
{
autoComplete.ReleaseAutoComplete();
autoComplete = new CustomSource(suggestions.Select(t => prefix + t).ToArray());
autoComplete.Bind(textBox1);
}

textChanging = false;
}
}
}
}


Hopefully this will help some of you who've been vexed by this issue in the past. Good luck!

Wednesday, July 1, 2009

I'm an MVP!

So, I received an email from Microsoft this morning. Apparently I've been awarded Microsoft's Most Valuable Professional (MVP) Award in Visual C#! Thanks Microsoft!

I plan to just "keep on keepin' on" and continue helping people on the MSDN Forums as best as I know how, and to continue providing the best content I know how to give here on my blog.

Wednesday, June 24, 2009

How to Load an XmlDocument and Completely Ignore DTD

A question came up on the Forums today from someone looking to ignore the DOCTYPE tag on an XML file while loading an XML file into an XmlDocument class instance without first reading the whole file and using something like Regex to replace the element. In other words, he was looking for a fast performing solution.

The XmlDocument class loads XML files via the Load or LoadXml methods, which all ultimately convert to an XmlTextReader before reading the XML. There's one exception to this rule, however, and that's the Load overload that accepts an XmlReader.

More than this, it's the XmlReader, and not the XmlDocument that resolves DTD validation arguments. It does this by using the XmlResolver set in the XmlReaderSettings.XmlResolver property.

To solve this issue, create an instance of XmlReaderSettings, and allow DTD processing by setting ProhibitDTD to false, but then remove the ability for the XmlReader to resolve the address specified in the DOCTYPE element by setting the XmlResolver property to null. After doing this, you can safely create an XmlReader, and pass the reader into the Load method of the XmlDocument, and the XmlDocument will load the specified XML file without validating the document.

The following code assumes you have your XML file loaded into a Stream named "xmlStream".


// Create an XmlReaderSettings object.  
XmlReaderSettings settings = new XmlReaderSettings();

// Set XmlResolver to null, and ProhibitDtd to false.
settings.XmlResolver = null;
settings.ProhibitDtd = false;

// Now, create an XmlReader. This is a forward-only text-reader based
// reader of Xml. Passing in the settings will ensure that validation
// is not performed.
XmlReader reader = XmlTextReader.Create(xmlStream, settings);

// Create your document, and load the reader.
XmlDocument doc = new XmlDocument();
doc.Load(reader);

Tuesday, June 16, 2009

Forums Browser Latest Release!

I just released another version of the Forums Browser application, and this one seems relatively stable. Most of the issues from the first release are resolved at this point, and I believe I have exactly one item left on the issues list at this point.

Anyways, here's the link to the latest version:

Forums Browser

Enjoy!

Wednesday, June 10, 2009

ForumsBrowser - It's Out!

I'm pleased to announce the opening of the ForumsBrowser page on CodePlex. On it, I've added the source code, and an initial release of the software as an installable msi file.

Be forewarned, however, that this software is still in development, so it's probably going to crash on you. :) Lemme know if it does. (A complete stack trace would be wonderful if you can manage to get it :) ).

Anyways, here it is!

http://forumsbrowser.codeplex.com/

Also, let me know if you're interested in contributing.

Monday, June 8, 2009

An Updated Preview of a Forums Browser

Well, I've been working on the MSDN Forums Browser a bit more. Here's what it looks like so far. Still needs some refactoring though....