Introduction
In this article, you will learn how to intercept an Outlook dialog and replace it with your own .NET form. The Outlook Object Model itself exposes no events and objects to replace the built-in dialogs with your own. However, by combining VSTO, P/Invoke, and .NET technologies, you have the ability to replace any kind of Outlook built-in dialog you can imagine.
Benefits
- Simple use of external data for address selection: CRM, SQL, Web Services, XML files, Outlook tables, etc.
- Don't have to implement a complicated COM address book provider
- No use of COM components which have to be registered at deployment
- Your own designer user interface
- Your own business logic in address selection, such as searching and resolving names
Drawbacks
- Use of complicated unmanaged code
- Depends on the version and language of your installed Office
Background
The Microsoft Outlook Object Model (OOM) is powerful, and provides access to many features that use and manipulate the data stored in Outlook and on the Exchange Server. Here are some common options to use external data in Microsoft Outlook:
- Importing the data into Outlook Items (Contacts)
- Importing data into the Exchange store using WebDAV or CDOSys
- Creating your own address book provider
Importing / Exporting data is time consuming and a reason for synchronization conflicts. The data is outdated and out of sync. Here is a new scenario that shows how to intercept an Outlook built-in dialog and replace it with your own.
The idea
What you definitely can get from the Outlook Object Model are Explorer and Inspector objects which represent application and data item windows. Luckily, whenever such a window is activated by the user (when someone clicks on it), or when it is deactivated (when another window comes to the front), you will receive events from these objects. You can use these events to get notified when windows are activated or deactivated. This is also true when a user clicks on the "To" or "Cc" button to select a recipient from the Recipient dialog. Whenever the Address / Recipient dialog is shown, your inspector window is deactivated. You can intercept and search for the opened Recipient dialog in the Deactivate event handler, close the Recipient dialog, and open your own .NET form instead.
Set up the solution
Before you can start hacking into Outlook, you have to install the minimum requirements on your development machine.
Prerequisites
Create a solution
To demonstrate this technique, start an Outlook AddIn project. Here in my case, I have Outlook 2007 (German) and Visual Studio 2008 Beta 2 running on Vista 64 bit.
After you have created the project, you will find a skeleton class called ThisAddIn
with two methods where the application is started and terminated. Here, the journey begins, and we will start to code the application.
namespace CustomAddressDialog
{
public partial class ThisAddIn
{
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
}
#region VSTO generated code
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
}
The Outlook InspectorWrapper template
As you can read in many articles, one of the most common problems when programming Outlook add-ins is the fact that you can have multiple Explorers and Inspectors opened and closed at any time during the lifetime of your Outlook session. One method of handling this situation correctly is by using an Explorer/Inspector wrapper which encapsulates each of these windows and traps the events and states of the different windows during their lifetime, and which does a proper cleanup to avoid ghost instances and crashes of your Outlook application. This technique is also common for IExtensibility
add-ins. Reference: InspectorWrapper Sample(H. Obertanner).
The Inspector/Explorer wrapper template is basically a wrapper class which has a unique ID, holds a reference to the wrapped object inside the class, monitors the object state, and informs the application when the object has been closed. Here it goes:
public partial class ThisAddIn
{
Outlook.Inspectors _Inspectors;
Outlook.Explorers _Explorers;
Dictionary<guid,WrappedObject> _WrappedObjects;
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
_WrappedObjects = new Dictionary<guid,WrappedObject>();
_Inspectors = this.Application.Inspectors;
for (int i = _Inspectors.Count; i >= 1; i--)
{
WrapInspector(_Inspectors[i]);
}
_Inspectors.NewInspector += new
Outlook.InspectorsEvents_NewInspectorEventHandler(_Inspectors_NewInspector);
_Explorers = this.Application.Explorers;
for (int i = _Explorers.Count; i >= 1; i--)
{
WrapExplorer(_Explorers[i]);
}
_Explorers.NewExplorer += new
Outlook.ExplorersEvents_NewExplorerEventHandler(_Explorers_NewExplorer);
}
void _Explorers_NewExplorer(Outlook.Explorer Explorer)
{
WrapExplorer(Explorer);
}
void WrapExplorer(Outlook.Explorer explorer)
{
ExplorerWrapper wrappedExplorer = new ExplorerWrapper(explorer);
wrappedExplorer.Closed += new WrapperClosedDelegate(wrappedObject_Closed);
_WrappedObjects[wrappedExplorer.Id] = wrappedExplorer;
}
void _Inspectors_NewInspector(Outlook.Inspector Inspector)
{
WrapInspector(Inspector);
}
void WrapInspector(Outlook.Inspector inspector)
{
InspectorWrapper wrappedInspector = new InspectorWrapper(inspector);
wrappedInspector.Closed += new WrapperClosedDelegate(wrappedObject_Closed);
_WrappedObjects[wrappedInspector.Id] = wrappedInspector;
}
void wrappedObject_Closed(Guid id)
{
_WrappedObjects.Remove(id);
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
_WrappedObjects.Clear();
_Inspectors.NewInspector -= new
Outlook.InspectorsEvents_NewInspectorEventHandler(_Inspectors_NewInspector);
_Inspectors = null;
_Explorers.NewExplorer -= new
Outlook.ExplorersEvents_NewExplorerEventHandler(_Explorers_NewExplorer);
_Explorers = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
#region VSTO generated code
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
The abstract WrapperClass
:
public delegate void WrapperClosedDelegate(Guid id);
internal abstract class WrapperClass
{
public event WrapperClosedDelegate Closed;
public Guid Id { get; private set; }
protected void OnClosed()
{
if (Closed != null) Closed(Id);
}
public WrapperClass()
{
Id = Guid.NewGuid();
}
}
The Inspector
wrapper class:
internal class InspectorWrapper : WrapperClass
{
public Outlook.Inspector Inspector { get; private set; }
public InspectorWrapper(Outlook.Inspector inspector)
{
Inspector = inspector;
ConnectEvents();
}
void ConnectEvents()
{
((Outlook.InspectorEvents_10_Event)Inspector).Close +=
new Outlook.InspectorEvents_10_CloseEventHandler(InspectorWrapper_Close);
((Outlook.InspectorEvents_10_Event)Inspector).Activate +=
new Outlook.InspectorEvents_10_ActivateEventHandler(InspectorWrapper_Activate);
((Outlook.InspectorEvents_10_Event)Inspector).Deactivate +=
new Outlook.InspectorEvents_10_DeactivateEventHandler(InspectorWrapper_Deactivate);
}
void DisconnectEvents()
{
((Outlook.InspectorEvents_10_Event)Inspector).Close -=
new Outlook.InspectorEvents_10_CloseEventHandler(InspectorWrapper_Close);
((Outlook.InspectorEvents_10_Event)Inspector).Activate -=
new Outlook.InspectorEvents_10_ActivateEventHandler(InspectorWrapper_Activate);
((Outlook.InspectorEvents_10_Event)Inspector).Deactivate -=
new Outlook.InspectorEvents_10_DeactivateEventHandler(InspectorWrapper_Deactivate);
}
void InspectorWrapper_Close()
{
DisconnectEvents();
Inspector = null;
GC.Collect();
GC.WaitForPendingFinalizers();
OnClosed();
}
void InspectorWrapper_Activate()
{
}
void InspectorWrapper_Deactivate()
{
}
}
The Explorer wrapper class is similar to the Inspector wrapper class. Refer to the sample solution to see additional details. In fact, what you now have is a small framework which could be used to successfully build your VSTO add-ins.
Search the built-in Recipient dialog
Now that you have arranged to be informed when your Inspector window becomes inactive (because you will receive the Deactivate event), you can search for the Recipient dialog now. You can't do it with .NET managed code - you have to use the good old Windows API for it. This technique is called P/Invoke, and it's the way to access unmanaged API DLL functions, methods, and callbacks from your managed code. The best online resources for information about P/Invoke are the MSDN Windows API documentation and a website called pinvoke.net.
Before you can search for the dialog/window, you have to know what to search for. Luckily, with Visual Studio, you get a small tool called Spy++. You can use this tool to search for windows, messages, and even to find the parent and child windows of any window. Start Microsoft Outlook, create a new mail, and select a recipient. When the built-in Recipient dialog is shown, start the Spy++ tool. It's usually located under "C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\Tools folder" on your hard disk. Now, you can use the "Find Window" function and drag the target onto your Recipient dialog.
Using Spy++ to get information about the Recipient dialog:
What you will get is some information about the window you selected. You will get information about the window text, the class name, and the handle. The handle is a dynamically assigned unique address of the window in your system. Because it's dynamically assigned, it changes every time the dialog is opened, and therefore isn't helpful here. The caption (title or window text) changes depending on the application context, and doesn't help us here either. How can you identify the window? The answer is not 42 - it's by the class name and by its child windows.
Here now is a small challenge for you: Since I coded this sample with a localized version of Outlook, you have to modify the code to suit your needs and locality. All controls on the Recipient dialog are windows too, they are child windows of the Recipient dialog. The next snippet demonstrates how to use some Windows API functions to:
- search for a window handle with the class name #32770
- enumerate all child windows of the dialog
- retrieve the window text of all the child windows
- see if all the required children are there to successfully identify the Recipient dialog
The method to retrieve a list of all child windows and their window text is encapsulated in a managed method to keep all API calls inside of a class.
Let's roll - here's the code for the WinApiProvider
class:
[SuppressUnmanagedCodeSecurity]
internal class WinApiProvider
{
[DllImport("user32", CharSet = CharSet.Auto)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32", CharSet = CharSet.Auto)]
public static extern int GetWindowText(IntPtr hWnd,
StringBuilder lpString, int nMaxCount);
public static List<string> GetWindowNames(List<IntPtr><intptr /> windowHandles)
{
List<string> windowNameList = new List<string>();
StringBuilder windowName = new StringBuilder(260);
foreach (IntPtr hWnd in windowHandles)
{
int textLen = GetWindowText(hWnd, windowName, 260);
windowNameList.Add(windowName.ToString());
}
return windowNameList;
}
public static List<IntPtr> EnumChildWindows(IntPtr hParentWnd)
{
List<intptr /> childWindowHandles = new List<intptr />();
GCHandle hChilds = GCHandle.Alloc(childWindowHandles);
try
{
EnumWindowProc childProc = new EnumWindowProc(EnumWindow);
EnumChildWindows(hParentWnd, childProc, GCHandle.ToIntPtr(hChilds));
}
finally
{
if (hChilds.IsAllocated)
hChilds.Free();
}
return childWindowHandles;
}
[DllImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumChildWindows(IntPtr hWnd,
EnumWindowProc callback, IntPtr userObject);
private static bool EnumWindow(IntPtr hChildWindow, IntPtr pointer)
{
GCHandle hChilds = GCHandle.FromIntPtr(pointer);
((List<intptr />)hChilds.Target).Add(hChildWindow);
return true;
}
public delegate bool EnumWindowProc(IntPtr hWnd, IntPtr parameter);
}
The interesting thing here is how to pass a managed generic list to an unmanaged API function by allocating an unmanaged handle to your managed object.
Now, you want to use it in your InspectorWrapper
to identify the window. Every time your Inspector is deactivated, let's go and search for the dialog.
The Event sink for the Inspector Deactivate method will look like this:
void InspectorWrapper_Deactivate()
{
IntPtr hBuiltInDialog = WinApiProvider.FindWindow("#32770", "");
if (hBuiltInDialog != IntPtr.Zero)
{
List<intptr> childWindows = WinApiProvider.EnumChildWindows(hBuiltInDialog);
List<string> childWindowsText = WinApiProvider.GetWindowNames(childWindows);
if (!childWindowNames.Contains("Nur N&ame")) return;
if (!childWindowNames.Contains("&Mehr Spalten")) return;
if (!childWindowNames.Contains("A&dressbuch")) return;
}
}</intptr>
Closing the built-in dialog
You have mastered the first exercise - identify the built-in dialog. You have the handle to it, and now you have to close the dialog. When you have a managed .NET form, this is easy - but if not, it's a little trickier. In the Windows API, two methods are documented:
You can't use either of them. Why? When you are receiving this event, the built-in dialog is not initialized completely and it runs in another thread. But, the whole Windows system is based on a message loop where windows exchange messages to interact together. So, you simply send the built-in dialog a Close message. This is the same effect as pressing ESC on the visible window. The window frees all used resources and closes properly. When the window has been closed, your Inspector window will become active again and you will receive an Inspector_Activated
event.
In the next code block, you will see how to close the window and the activate method that is used to display our own dialog:
void InspectorWrapper_Deactivate()
{
_showOwnDialogOnActivate = false;
IntPtr hBuiltInDialog = WinApiProvider.FindWindow("#32770", "");
if (hBuiltInDialog != IntPtr.Zero)
{
List<intptr /> childWindows = WinApiProvider.EnumChildWindows(hBuiltInDialog);
List<string> childWindowNames = WinApiProvider.GetWindowNames(childWindows);
if (!childWindowNames.Contains("Nur N&ame")) return;
if (!childWindowNames.Contains("&Mehr Spalten")) return;
if (!childWindowNames.Contains("A&dressbuch")) return;
WinApiProvider.SendMessage(hBuiltInDialog,
WinApiProvider.WM_SYSCOMMAND, WinApiProvider.SC_CLOSE, 0);
_showOwnDialogOnActivate = true;
}
}
bool _showOwnDialogOnActivate;
void InspectorWrapper_Activate()
{
if (_showOwnDialogOnActivate)
{
RecipientDialog customDialog = new RecipientDialog();
customDialog.ShowDialog();
}
}
The picture below shows the design of the .NET form used to replace the built-in dialog.
Basically, it has a DataGridView
, To, Cc, and Bcc buttons with corresponding textboxes, and a Search button with a combo box. You want to have cool looking modern functionality, and so we want to filter the Recipient list while typing into the combo box. The combo box should display the last used search phrases.
To realize the filtering, you can use a DataView
with an attached DataSet
. The DataSet
can be easily designed with Visual Studio and used as the DataSource
for the DataView
. So, go ahead and add a new DataSet
to the application with the specific fields for the recipients.
Different data sources
The implementation of this dialog is whatever you can imagine - it depends on what your requirements are. Just to give you a start, you will use three different data sources to fill up your new Recipients dialog in this sample.
- Internal Outlook data by using the Table object (Outlook 2007 only)
- An external XML file
- SQL data using LINQ to SQL with a corresponding database
First, you will access the data of the Contacts folder. In the past, you had one of these options to access the Outlook internal data:
- Loop over the folder Items (very slow and problematic with 250 RPC connections normally allowed)
- Like option 1, but caching the data (synchronization required)
- Use of CDO (not supported, security violations)
- Use of a third party DLL such as Redemption from Dmitry Streblechenko
New in Outlook 2007 is a Table object which provides fast access to an Outlook folder's contents. You will use it as shown below to get the contents of the personal Contacts folder and populate the custom dialog. The helper methods are in a class called OutlookUtility
. You have to pass the name of the columns you want to retrieve, and you can apply a filter on the table items.
The implementation looks like this:
internal class OutlookUtility
{
public static Outlook.Table GetFolderTable(Outlook.OlDefaultFolders defaultFolder,
string filter)
{
Outlook.MAPIFolder folder =
Globals.ThisAddIn.Application.Session.GetDefaultFolder(defaultFolder);
return GetFolderTable(folder, filter);
}
public static Outlook.Table GetFolderTable(Outlook.MAPIFolder folder, string filter)
{
return folder.GetTable(filter, Missing.Value);
}
public static void SetTableColumns(Outlook.Table table, string[] columnNames)
{
table.Columns.RemoveAll();
foreach (string columnName in columnNames)
{
table.Columns.Add(columnName);
}
}
}
Now, take a closer look at the implementation of the .NET form. As mentioned earlier, you will use a backgroundworker to pump the data into your new dialog. Also, you have to connect the dialog to your Inspector's data, so that the recipients that you have selected shows up in the mail somehow and vice versa.
You can achieve this by passing the Inspector's CurrentItem
object to the form and by modifying the item directly within the Recipient dialog. The corresponding code is shown below:
public partial class RecipientDialog : Form
{
object _item;
public RecipientDialog(object item)
{
InitializeComponent();
_item = item;
ProcessPropertyTags(false);
}
private void ProcessPropertyTags(bool write)
{
foreach (Control c in this.Controls)
{
if (!string.IsNullOrEmpty(c.Tag as string))
{
if (write)
{
OutlookUtility.PropertySet(ref _item, (string)c.Tag, c.Text);
}
else
{
c.Text = OutlookUtility.PropertyGet(ref _item,
(string)c.Tag).ToString();
}
}
}
}
private void OKButton_Click(object sender, EventArgs e)
{
ProcessPropertyTags(true);
DialogResult = DialogResult.OK;
this.Close();
}
private void Cancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
this.Close();
}
private void Form_FormClosed(object sender, FormClosedEventArgs e)
{
_item = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
Now, you have the dialog connected to your Inspector and you should fill it with data. You want to maintain a responsive application, so the decision is to use a backgroundworker for your application. You start with Outlook Contacts Folder data. The theory says: create a background thread, get the folder table, loop over the data, and add it to your dataset. While looping over the data, show the progressbar. When finished, enable all user-elements.
Study the more advanced code of the backgroundworker process:
DataView _dvContacts;
bool _outlookLoaderFinished;
private void _outlookContactLoader_DoWork(object sender, DoWorkEventArgs e)
{
Outlook.Table contactsTable =
OutlookUtility.GetFolderTable(Outlook.OlDefaultFolders.olFolderContacts,
"[MessageClass] = 'IPM.Contact'");
OutlookUtility.SetTableColumns(ref contactsTable, new string[]
{ "EntryID", "FirstName", "LastName",
"CompanyName", "User1", "Email1Address" });
int itemCount = contactsTable.GetRowCount();
int count = 0;
while (!contactsTable.EndOfTable && !e.Cancel)
{
count++;
Outlook.Row row = contactsTable.GetNextRow();
string entryId = row[1] as string;
string firstName = row[2] as string;
string lastName = row[3] as string;
string company = row[4] as string;
string customerId = row[5] as string;
string email = row[6] as string;
_dsContacts.ContactTable.AddContactTableRow(entryId, firstName,
lastName, email, company, customerId);
_outlookContactLoader.ReportProgress(((int)count * 100 / itemCount));
}
}
private void _outlookContactLoader_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
_outlookLoaderFinished = true;
RefreshUI();
}
private void _outlookContactLoader_ProgressChanged(object sender,
ProgressChangedEventArgs e)
{
UpdateProgress(e.ProgressPercentage);
}
private delegate void UpdateProgressDelegate(int progress);
private void UpdateProgress(int progress)
{
if (ProgressBarStatus.InvokeRequired)
{
this.Invoke(new UpdateProgressDelegate(UpdateProgress), progress);
}
ProgressBarStatus.Value = progress;
ProgressBarStatus.Update();
ResultGrid.DataSource = _dvContacts;
}
private void RefreshUI()
{
bool allLoadersfinished = (_outlookLoaderFinished);
ToButton.Enabled = CcButton.Enabled = BccButton.Enabled = allLoadersfinished;
ProgressBarStatus.Visible = !allLoadersfinished;
_dvContacts.RowFilter = GetRowFilterText(SearchTextComboBox.Text);
ResultGrid.DataSource = _dvContacts;
}
private string GetRowFilterText(string searchText)
{
if (string.IsNullOrEmpty(searchText)) searchText = "*";
return "[FirstName] LIKE '*" + searchText +
"*' OR [LastName] LIKE '*" + searchText +
"*' OR [CompanyName] LIKE '*" + searchText +
"*' OR [EmailAddress] LIKE '*" + searchText + "*'";
}
Take a break now. You should have an initial functional add-in now, and the design goals are reached to this point (with minor bugs). You can download this solution as Part 1 from here now and study the code.
Resume - In the first part, we discussed:
- Create a VSTO Outlook application add-in
- The Inspector/Explorer wrapper template
- Use unmanaged API calls to deal with Outlook windows beyond the Outlook Object Model
- The Outlook 2007 Table object
- Use of a backgroundworker to keep a responsive user interface
Using LINQ to SQL to query external data
You want to use an external SQL database for your addresses. Fine - let's create one. In an enterprise, usually, you would use a central SQL Server. Here you use a local database, created by yourself with the SQL Server Express Edition and the new LINQ language extensions.
First, you need to add a reference to the System.Data.Linq DLL.
In this scenario, you have just one simple entity, so call it "Customer". You will create a fresh database if one doesn't exist already and add some customers to it. Also, two methods to retrieve the data back from the database into entities would be helpful for the application. Note: All DB related classes are placed in a subfolder/namespace called "Database".
The Customer
class:
[Table(Name = "Customers")]
public class Customer
{
[Column(IsPrimaryKey = true, IsDbGenerated = true)]
public int CustomerId { get; set; }
[Column(CanBeNull = false)]
public string Firstname { get; set; }
[Column(CanBeNull = false)]
public string Lastname { get; set; }
[Column (CanBeNull = false)]
public string Emailaddress { get; set; }
public string Companyname { get; set; }
}
The CustomAddressDialogDB
class, inherited from DataContext
:
public class CustomAddressDialogDB : DataContext
{
public CustomAddressDialogDB(string fileOrServerConnection)
: base(fileOrServerConnection)
{
#if DEBUG
if (DatabaseExists())
{
DeleteDatabase();
GC.Collect();
GC.WaitForPendingFinalizers();
}
#endif
if (!DatabaseExists())
{
CreateDatabase();
AddCustomer("Ken", "Slovak", "some.address@somedomain.com", "Slovaktech");
AddCustomer("Sue", "Mosher", "some.address@otherdomain.com", "Turtleflock");
AddCustomer("Dmitry", "Streblechenko",
"another.address@some.otherdomain.com", "Streblechenko");
AddCustomer("Randy", "Byrne", "unknown@address.com", "Microsoft");
}
}
public Table<Customer> _customerTable;
public int AddCustomer(string firstname, string lastname,
string emailaddress, string companyname)
{
Customer customer = new Customer();
customer.Firstname = firstname;
customer.Lastname = lastname;
customer.Emailaddress = emailaddress;
customer.Companyname = companyname;
_customerTable.InsertOnSubmit(customer);
SubmitChanges();
return customer.CustomerId ;
}
public List<Customer> FindCustomers(string query, int maxItems)
{
var q = from customer in _customerTable
where customer.Lastname.Contains(query)
|| customer.Firstname.Contains(query)
|| customer.Emailaddress.Contains(query)
orderby customer.Lastname, customer.Firstname
select customer;
return q.Take(maxItems).ToList<Customer>();
}
public List<Customer> GetCustomers()
{
var q = from customer in _customerTable
orderby customer.Lastname, customer.Firstname
select customer;
return q.ToList<Customer>();
}
}
In your customized dialog, you will use the new data source and create a new backgroundworker that will load the data from the database and fill up your Address dialog with data.
What you are missing is the connection string for your database. You have to save the database somehow where you have the possibility to write files. Where depends on how restricted your account on your system is - at the minimum, you can write to the MyDocuments folder. How can you get it? In .NET 3.5, this is easy with:
public static string GetMyDocumentsFolder(){
return Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments );
}
The corresponding directory for your application documents would be something like:
string dataPath = Path.Combine(OutlookUtility.GetMyDocumentsFolder (),
"CustomAddressDialog");
if (!Directory.Exists(dataPath)) Directory.CreateDirectory(dataPath);
Caution! There is a problem here when you try to create your database on the fly - you will receive a no access exception. This is because the SQLEXPRESS instance has no rights to access your personal directory, by default. But, you are smart and give the service the rights to do something in this directory.
Just to make it easy, you give control to the Networkservice here (in German, Netzwerkdienst):
DirectoryInfo di = new DirectoryInfo(dataPath);
DirectorySecurity acl = di.GetAccessControl();
acl.AddAccessRule (new FileSystemAccessRule ("Networkservice",
FileSystemRights.FullControl, AccessControlType.Allow ));
di.SetAccessControl(acl);
dataPath = Path.Combine(dataPath, "CustomAddressDialogDB.mdf");
Database.CustomAddressDialogDB db =
new CustomAddressDialog.Database.CustomAddressDialogDB(dataPath );
Now, the first time when your customized dialog is opened, a fresh database with some sample data is created and is ready for use.
The next line shows you how to get the data out of the server:
List<database.Customer> customers = db.GetCustomers ();
That was easy? The rest of this backgroundworker is very similar to the Outlook ones, you can refer to the sample code if you like. Resume for Part 2:
- Create a simple business entity using the new LINQ language features
- Add correct user credentials for the Networkservice to the application data folder
- Create a SQL database from scratch
- Retrieve SQL data using LINQ language features
- Extend the custom select Recipient dialog with a second data source
Download the solution, part 2, with source code below:
Outlook specific tweaks
As you can see, I'm a German guy, and I'm using a localized version of Microsoft Outlook. However, how do you find out if you have a German or an English version currently running? In the Outlook 2007 Object Model, there is a property called Application.LanguageSettings
. A language ID of 1031 (0x407) stands for German - an ID of 1033 (0x409), for the English version of Outlook. Many thanks to Ken Slovak here for providing me with the English screenshots of the Select Recipients dialog. The following code snippet will give you an idea of how you can retrieve the local language:
int languageId = Inspector.Application.LanguageSettings.get_LanguageID(
Microsoft.Office.Core.MsoAppLanguageID.msoLanguageIDUI);
switch (languageId)
{
case 1031:
if (!childWindowNames.Contains("Nur N&ame")) return;
if (!childWindowNames.Contains("&Mehr Spalten")) return;
if (!childWindowNames.Contains("A&dressbuch")) return;
break;
case 1033:
if (!childWindowNames.Contains("N&ame only")) return;
if (!childWindowNames.Contains("Mo&re Columns")) return;
if (!childWindowNames.Contains("A&ddress Book")) return;
break;
default:
return;
}
Now, whenever you click the "To", "Cc", or "Bcc" button on an Inspector window, you will notice that before your own dialog shows up, you will see the original Select Recipient dialog flashing up in the background. The only way to suppress this window showing up is by using a bad trick. We have to create a new invisible window and make the original window a child of it. You need two additional API calls - one for creating a new window and one for changing the parent of a window.
Here are the needed API calls:
[DllImport("user32")]
public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[DllImport("user32.dll")]
public static extern IntPtr CreateWindowEx(
uint dwExStyle,
string lpClassName,
string lpWindowName,
uint dwStyle,
int x,
int y,
int nWidth,
int nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
}
And, here you can see the modified InspectorWrapper
class:
_hWndInvisibleWindow = WinApiProvider.CreateWindowEx(0, "Static",
"X4UTrick", 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
WinApiProvider.SetParent(hBuiltInDialog, _hWndInvisibleWindow);
WinApiProvider.SendMessage(hBuiltInDialog,
WinApiProvider.WM_SYSCOMMAND, WinApiProvider.SC_CLOSE, 0);
_showOwnDialogOnActivate = true;
}
}
IntPtr _hWndInvisibleWindow;
bool _showOwnDialogOnActivate;
void InspectorWrapper_Activate()
{
if (_showOwnDialogOnActivate)
{
WinApiProvider.SendMessage(_hWndInvisibleWindow,
WinApiProvider.WM_SYSCOMMAND, WinApiProvider.SC_CLOSE, 0);
RecipientDialog customDialog = new RecipientDialog(Inspector.CurrentItem);
customDialog.ShowDialog();
}
}
Now, the annoying original dialog is gone. That's it for now. With this technique, now you have the opportunity to change any dialog within Outlook. This is also true for Print dialogs, etc. Now, go on and extend your Outlook customization with more functionality and added benefit for your customers.
Resume of Part 3:
- Determining the current UI setting of your Outlook instance
- Suppress the annoying original Select Recipients dialog flashing up by changing the parent window
Download Part 3 with Outlook tweaks below:
Notes
The following notes are valid for this and all VSTO add-ins:
- The temporary key for this solution was created on my development machine. You have to create and use your own.
- The solution has no Setup project. For distributing VSTO add-ins, see Deploying VSTO Solutions.
- For each DLL that is used from your add-in, you have to set the security policy (custom action in MSI Setup package).
Special thanks to:
History
- V.1.0 - Initial version (13 November, 2007).
- V.1.1 - Spelling corrections (Special thanks to Ken Slovak) (16 November, 2007).
- V.1.2 - Upgraded projects to support Visual Studio 2008 RTM with VSTO (11 March, 2008).