I'm developing an application that needs to perform some processing on the user's Outlook contacts. I'm currently accessing the list of Outlook contacts by iterating over the result of MAPIFolder.Items.Restrict(somefilter), which can be found in Microsoft.Office.Interop.Outlook.
In my application, my user needs to choose several contacts to apply a certain operation on. I would like to add a feature that will allow the user to drag a contact from Outlook and drop it on a certain ListBox in the UI (I work in WPF but this is probably is more generic issue).
I'm very new to C# and WPF - how can I:
Receive a dropped item on a ListBox
Verify it's a ContactItem (or something that wraps ContactItem)
Cast the dropped item to a ContactItem so I can process it
Thanks
I tried this with a TextBox (no difference with a ListBox in practice).
Summary :
Searching in all outlook contacts for the one recieved dragged as text.
The search here is based on the person's FullName.
condition(s):
When you drag a contact, it must show the FullName when selected in outlook. The only catch is when two persons have the same full names!! If it's the case you can try to find a unique identifier for a person by combining ContactItem properties and searching them in the dragged text.
private void textBox1_DragDrop(object sender, DragEventArgs e)
{
if (e.Data.GetData("Text") != null)
{
ApplicationClass app;
MAPIFolder mapif;
string contactStr;
contactStr = e.Data.GetData("Text").ToString();
app = new ApplicationClass();
mapif = app.GetNamespace("MAPI").GetDefaultFolder(OlDefaultFolders.olFolderContacts);
foreach (ContactItem tci in mapif.Items)
{
if (contactStr.Contains(tci.FullName))
{
draggedContact = tci; //draggedContact is a global variable for example or a property...
break;
}
}
mapif = null;
app.Quit;
app = null;
GC.Collect();
}
}
of course this code is to be organized-optimized, it's only to explain the method used.
You can try using the Explorer.Selection property combined with GetData("Text") [to ensure it's coming from Outlook, or you can use GetData("Object Descriptor") in the DragOver Event, decode the memory stream, search for "outlook", if not found cancel the drag operation] And why not drag multiple contacts!
private void textBox1_DragDrop(object sender, DragEventArgs e)
{
if (e.Data.GetData("Text") != null)
{
ApplicationClass app;
Explorer exp;
List<ContactItem> draggedContacts;
string contactStr;
contactStr = e.Data.GetData("Text").ToString();
draggedContacts = new List<ContactItem>();
app = new ApplicationClass();
exp = app.ActiveExplorer();
if (exp.CurrentFolder.DefaultItemType == OlItemType.olContactItem)
{
if (exp.Selection != null)
{
foreach (ContactItem ci in exp.Selection)
{
if (contactStr.Contains(ci.FullName))
{
draggedContacts.Add(ci);
}
}
}
}
app = null;
GC.Collect();
}
}
An Outlook contact, when dropped, supports the following formats:
(0): "RenPrivateSourceFolder"
(1): "RenPrivateMessages"
(2): "FileGroupDescriptor"
(3): "FileGroupDescriptorW"
(4): "FileContents"
(5): "Object Descriptor"
(6): "System.String"
(7): "UnicodeText"
(8): "Text"
The most interesting looking one on that list (for me) is Object Descriptor, which then led me to someone with a similar sounding problem:
http://bytes.com/topic/visual-basic-net/answers/527320-drag-drop-outlook-vb-net-richtextbox
Where it looks like in that case, they detect that it's an Outlook drop, and then use the Outlook object model to detect what's currently selected, with the implication that that must be the current drop source.
What you could probably do is accept the drag and drop event in your .wpf app and then get the selected item(s) from outlook and pull them into your application.
Update
Add the Outlook PIA references to you app.
Microsoft.Office.Interop.Outlook.Application app = new Microsoft.Office.Interop.Outlook.Application();
Microsoft.Office.Interop.Outlook.Explorer activeExplorer = app.ActiveExplorer();
Microsoft.Office.Interop.Outlook.Selection currentSelection = activeExplorer.Selection;
Then you can iterate over the currentSelection collection to see what the user has dragged over.
Related
I create a desktop app c# where i use some references :
using Microsoft.Toolkit.Uwp.Notifications;
using System.Windows;
using Windows.ApplicationModel.Activation;
using Microsoft.QueryStringDotNET;
And where i added some reference related to UWP application :
- Windows.System
- Windows.UI
- Windows.data
- Windows.Foundation
- Windows.ApplicationModel
Then i created a simple procedure to create and show my toast notification with the followin code :
private void Button_Click(object sender, RoutedEventArgs e)
{
var toastContent = new ToastContent()
{
Visual = new ToastVisual()
{
BindingGeneric = new ToastBindingGeneric()
{
Children =
{
new AdaptiveText()
{
Text = "Surface Launch Party"
},
new AdaptiveText()
{
Text = "Studio S / Ballroom"
},
new AdaptiveText()
{
Text = "4:00 PM, 10/26/2015"
}
}
}
},
Actions = new ToastActionsCustom()
{
Inputs =
{
new ToastSelectionBox("status")
{
DefaultSelectionBoxItemId = "yes",
Items =
{
new ToastSelectionBoxItem("yes", "Going"),
new ToastSelectionBoxItem("maybe", "Maybe"),
new ToastSelectionBoxItem("no", "Decline")
}
}
},
Buttons =
{
new ToastButton("RSVP", "action=rsvpEvent&eventId=63851")
{
ActivationType = ToastActivationType.Foreground
},
new ToastButtonDismiss()
}
},
Launch = "action=viewEvent&eventId=63851"
};
Windows.Data.Xml.Dom.XmlDocument xmldoc = new Windows.Data.Xml.Dom.XmlDocument();
xmldoc.LoadXml(toastContent.GetContent());
var toast = new ToastNotification(xmldoc);
toast.Activated += OnActivated1;
// Create the toast notification
//var toastNotif = new ToastNotification(xmlDoc);
// And send the notification
ToastNotificationManager.CreateToastNotifier("Test").Show(toast);
Now my problem is that i don't know how to retrieve the item i selected in the list :-(
I created an procedure based toast.Activated event :
void OnActivated1(ToastNotification sender, object e)
{
var toastActivationArgs = e as ToastNotificationActivatedEventArgs;
}
With this event, i can retrieve the argument (to know the button i clicked on) but getting the UserInput thanks to the class "ToastNotificationActivatedEventArgs" seems to be not possible...
Do you know if it's possible ? Is it a limitation of using reference UWP in a desktop app ?
Thank you very much !
Vincent
If you're building a Win32 Desktop app using the Desktop Bridge, you currently cannot use inputs and selection boxes in your toast, as there's no way to retrieve the input.
If you're building a normal Win32 app, you must set up a COM server to handle activation, which will include the inputs that the user selected. This quickstart explains how to set this up for normal Win32 apps. Plus, this will also allow your toasts to persist in Action Center, so if the user missed the popup, they can still access your toast from Action Center.
thank you for your answers.
The issue i have with my sub OnActivated :
void OnActivated1(ToastNotification sender, object e)
{
var toastActivationArgs = e as ToastNotificationActivatedEventArgs;
}
is that the "e" object is recocgnized as a Windows.UI.Notifications.ToastActivatedEventsArgs and not as "ToastNotificationActivatedEventArgs" from Windows.ApplicationModel.Activation (where the properties Kind, UserInput.. should be very useful for me to get the content of selected item).
In my OnActivated1 sub, the var toastActivationArgs value equals to null because it cannot be converted to ToastNotificationActivatedEventArgs.
During my test the e.arguments equals to string "action=rsvpEvent&eventId=63851" but there is no XML returned. This is the only property available of the object e. (but this is useful to get the button where i clicked on)
I'm going to check the link from Andrew Bares to try to setup a COM server but i can see that's in c++ language..
Thank you !
The ToastNotificationActivatedEventArgs e.Arguments should be an xml string with what you need ready to parse.
First look at the string and see if it has what you need. Then go about using XMLReader or something to parse it.
Could you post the string you get?
I want to build an Outlook AddIn in C# that has a button in the calendar ribbon that the user clicks to create a new meeting with one of their employees. We want the user (the manager) to be able to select the employee from a filtered list of only their own employees and not have to search through the entire directory. What is the best way to do this?
addendum:
I did some searching and I came across a potential method for the filter.
I know that the "SelectNamesDialog" function will get me an Address Book dialog box:
Outlook.SelectNamesDialog snd = Application.Session.GetSelectNamesDialog();
I want to combine that with a piece of code I found. I modified it to return the names of all the manager's direct reports (the employees under the manager).
I think I'm on the right track, but I'm uncertain of what to do next. How do I now allow the user to select one of these names through the GetSelectNamesDialog? It is OK if your answer is in psuedocode.
// source: "How to: Get Information About Direct Reports of the Current User's Manager"
// https://msdn.microsoft.com/en-us/library/ff184617.aspx
private List<string> GetManagerDirectReports()
{
List<string> AddressNames = new List<string>();
Outlook.AddressEntry currentUser = Globals.ThisAddIn.Application.Session.CurrentUser.AddressEntry;
if (currentUser.Type == "EX")
{
Outlook.ExchangeUser manager = currentUser.GetExchangeUser().GetExchangeUserManager();
if (manager != null)
{
Outlook.AddressEntries addrEntries = manager.GetDirectReports();
if (addrEntries != null)
{
foreach (Outlook.AddressEntry addrEntry in addrEntries)
{
//System.Windows.Forms.MessageBox.Show(addrEntry.Name);
AddressNames.Add(addrEntry.Name);
}
}
}
}
return AddressNames;
}
Address book won't let you restrict the list to a subset of some users, so you'd need to come up with you own window that prompts the user for the selection from a pre-filtered list.
I now seem to be able to post an answer to my own question.
I added a dropdown in the Form Region and added this code to populate the dropdown with names of the manager's direct reports:
// Get Outlook list of employees who report to manager, using Exchange data.
List<string> mgrAddressNames = GetManagerDirectReports();
if (mgrAddressNames.Count >= 1)
{
try
{
// System.Windows.Forms.BindingSource bindingSource1;
// Create a Binding Source to the ComboBox to make values in ComboBox match the results of the list of direct reports.
System.Windows.Forms.BindingSource bindingSource1 = new System.Windows.Forms.BindingSource();
bindingSource1.DataSource = mgrAddressNames;
EmployeeInvited.DisplayMember = "Value";
EmployeeInvited.ValueMember = "Key";
EmployeeInvited.DataSource = bindingSource1.DataSource;
bindingSource1.Dispose();
(etc)
I'm creating a Microsoft Office 2013 Add-in that does some behaviour similar to the Address book in an Active Mail item:
I'm working with Microsoft.Office.Interop.Outlook.MailItem and using the Recipients property, and the Recipients.ResolveAll() method to load and remove addresses to it, everything seems to work fine until I click OK on my loaded from so I send control back to the active mail Item. At this point Outlook goes crazy and doesn't place the addresses properly in the To or the CC, or some of them go missing.
I'm thinking that one solution to the problem could be to trigger the Check Names when the user is ready to click OK and send all the addresses from my form to the Active mail item.
How can I trigger this action / button?
I couldn't find anything that could do that in the MailItem class. I would like something similar to this (but to use from within my poped-out windows form in Outloook):
UPDATE:
This are some bits of my code to add more context:
I use the method AddRecipientToActiveItem for each item that I have. It will verify if it already exists (it was already added), if not it will resolve it and if it is correct add it.
private void AddRecipientToActiveItem(string recipientAddress, Recipients recipientList, OlMailRecipientType recipientType)
{
Recipient recipientObject = default(Recipient);
if (!string.IsNullOrWhiteSpace(recipientAddress) && !EmailRecipientAlreadyExists(recipientAddress, recipientType))
{
recipientObject = recipientList.Add(recipientAddress);
recipientObject.Resolve();
if (recipientObject.Resolved)
{
recipientObject.Type = (int)recipientType;
recipientList.ResolveAll();
}
else
{
recipientObject.Delete();
}
}
}
for this, I have to iterate through each element in the list of recipients and compare by address + type (From,To,CC,BCC) pair:
private bool EmailRecipientAlreadyExists(string fullEmailAddress, OlMailRecipientType recipientType)
{
foreach (Microsoft.Office.Interop.Outlook.Recipient recipientObject in ActiveMailItem.Recipients)
{
if (GetRecipientEmailAddress(recipientObject) != null)
{
if (GetRecipientEmailAddress(recipientObject).Equals(fullEmailAddress) && recipientObject.Type == (int)recipientType)
return true;
}
}
return false;
}
But the user can also add addresses to their email item manually, and some of them exist in the exchange server, but others are simple smtp address, so when comparing I have to handle both scenarios:
private string GetRecipientEmailAddress(Microsoft.Office.Interop.Outlook.Recipient recipientObject)
{
Outlook.ExchangeUser objExchangeUser = null;
if (recipientObject.Address != null && recipientObject.AddressEntry != null)
objExchangeUser = recipientObject.AddressEntry.GetExchangeUser();
if (recipientObject.Address == null && objExchangeUser == null)
return recipientObject.Name;
if (objExchangeUser == null)
return recipientObject.Address;
return objExchangeUser.PrimarySmtpAddress;
}
}
It seems like this what I've done is not enough to keep consistent states of the email addresses when I read them from the mail item and put them in a text box of my form, then add more addresses and send them back to the mail item and close the form, and repeat the process.
The Namespace class provides the GetSelectNamesDialog method which obtains a SelectNamesDialog object for the current session. It displays the Select Names dialog box for the user to select entries from one or more address lists, and returns the selected entries in the collection object specified by the property SelectNamesDialog.Recipients.
Sub SelectRecipients()
Dim oMsg As MailItem
Set oMsg = Application.CreateItem(olMailItem)
Dim oDialog As SelectNamesDialog
Set oDialog = Application.Session.GetSelectNamesDialog
With oDialog
.InitialAddressList = _
Application.Session.GetGlobalAddressList
.Recipients = oMsg.Recipients
If .Display Then
'Recipients Resolved
oMsg.Subject = "Hello"
oMsg.Send
End If
End With
End Sub
The dialog box displayed by SelectNamesDialog.Display is similar to the Select Names dialog box in the Outlook user interface. It observes the size and position settings of the built-in Select Names dialog box. However, its default state does not show Message Recipients above the To, Cc, and Bcc edit boxes. For more information on using the SelectNamesDialog object to display the Select Names dialog box, see Display Names from the Address Book.
Thanks in advance.
Is it possible to activate a tab in another program using an IntPtr? If so, how?
SendKeys is not an option.
Perhaps what I need is a fishing lesson. I have exhausted Google and my lead developer.
I would appreciate an outright solution OR a recommendation to continue my Google efforts.
basic process is:
I drag a shortcut icon to the launcher
This opens the target application (Notepad++) and grabs IntPtr, etc.
I would like to programmatically select various items in Notepad++ such as Edit, menu items under Edit, or a doc tab.
The basic code I am running is:
the 'blob'
item 1: IntPtr of item
item 2: IntPtr of itemsChild
item 3: control text of item 1
item 4: is rectangle parameters of item 1
root contains similar info:
As others pointed out, the standard way of doing this is to use UI Automation. Notepad++ does support UI Automation (to some extent, as it's somehow automatically provided by the UI Automation Windows layers).
Here is a sample C# console app that demonstrates the following sceanrio (you need to reference UIAutomationClient.dll, UIAutomationProvider.dll and UIAutomationTypes.dll):
1) get the first running notepad++ process (you must start at least one)
2) open two files (note there may be already other opened tabs in notepad++)
3) selects all tabs in an infinite loop
class Program
{
static void Main(string[] args)
{
// this presumes notepad++ has been started somehow
Process process = Process.GetProcessesByName("notepad++").FirstOrDefault();
if (process == null)
{
Console.WriteLine("Cannot find any notepad++ process.");
return;
}
AutomateNpp(process.MainWindowHandle);
}
static void AutomateNpp(IntPtr handle)
{
// get main window handle
AutomationElement window = AutomationElement.FromHandle(handle);
// display the title
Console.WriteLine("Title: " + window.Current.Name);
// open two arbitrary files (change this!)
OpenFile(window, #"d:\my path\file1.txt");
OpenFile(window, #"d:\my path\file2.txt");
// selects all tabs in sequence for demo purposes
// note the user can interact with n++ (for example close tabs) while all this is working
while (true)
{
var tabs = GetTabsNames(window);
if (tabs.Count == 0)
{
Console.WriteLine("notepad++ process seems to have gone.");
return;
}
for (int i = 0; i < tabs.Count; i++)
{
Console.WriteLine("Selecting tab:" + tabs[i]);
SelectTab(window, tabs[i]);
Thread.Sleep(1000);
}
}
}
static IList<string> GetTabsNames(AutomationElement window)
{
List<string> list = new List<string>();
// get tab bar
var tab = window.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Tab));
if (tab != null)
{
foreach (var item in tab.FindAll(TreeScope.Children, PropertyCondition.TrueCondition).OfType<AutomationElement>())
{
list.Add(item.Current.Name);
}
}
return list;
}
static void SelectTab(AutomationElement window, string name)
{
// get tab bar
var tab = window.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Tab));
// get tab
var item = tab.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, name));
if (item == null)
{
Console.WriteLine("Tab item '" + name + "' has been closed.");
return;
}
// select it
((SelectionItemPattern)item.GetCurrentPattern(SelectionItemPattern.Pattern)).Select();
}
static void OpenFile(AutomationElement window, string filePath)
{
// get menu bar
var menu = window.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.MenuBar));
// get the "file" menu
var fileMenu = menu.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "File"));
// open it
SafeExpand(fileMenu);
// get the new File menu that appears (this is quite specific to n++)
var subFileMenu = fileMenu.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Menu));
// get the "open" menu
var openMenu = subFileMenu.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "Open..."));
// click it
((InvokePattern)openMenu.GetCurrentPattern(InvokePattern.Pattern)).Invoke();
// get the new Open dialog (from root)
var openDialog = WaitForDialog(window);
// get the combobox
var cb = openDialog.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.ComboBox));
// fill the filename
((ValuePattern)cb.GetCurrentPattern(ValuePattern.Pattern)).SetValue(filePath);
// get the open button
var openButton = openDialog.FindFirst(TreeScope.Children, new AndCondition(
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Button),
new PropertyCondition(AutomationElement.NameProperty, "Open")));
// press it
((InvokePattern)openButton.GetCurrentPattern(InvokePattern.Pattern)).Invoke();
}
static AutomationElement WaitForDialog(AutomationElement element)
{
// note: this should be improved for error checking (timeouts, etc.)
while(true)
{
var openDialog = element.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Window));
if (openDialog != null)
return openDialog;
}
}
static void SafeExpand(AutomationElement element)
{
// for some reason, menus in np++ behave badly
while (true)
{
try
{
((ExpandCollapsePattern)element.GetCurrentPattern(ExpandCollapsePattern.Pattern)).Expand();
return;
}
catch
{
}
}
}
}
If you wonder how this has been made, then you must read about UI Automation. The mother of all tools is called Inspect: https://msdn.microsoft.com/library/windows/desktop/dd318521.aspx
Make sure you get version at least 7.2.0.0. Note there is another one called UISpy but inspect is better.
Note, unfortunately, notepad++ tab text content - because it's based on the custom scintilla editor control - does not properly supports automation (we can't read from it easily, I suppose we'd have to use scintilla Windows messages for this), but it could be added to it (hey, scintilla guys, if you read this ... :).
In addition to the answer from Garath, you might also want to investigate the Windows automation API's i.e. the technology used to implement coded UI tests for GUI applications. As part of regular functional testing, I routinely control an external application from a set of NUnit tests using these API's.
Tools like UIAVerify will give you an indication of what controls are available in the application and you can use the Invoke Pattern (and many others) to interact with the controls at run-time.
If you want a detailed example of how to use the automation API's, the open source TestStack White project is pretty handy.
It is almost not possible if SendKeys is not an option but read more
Now more important part of the question- why:
We have to look how win32 application works: it has a WndProc/WindowProc method which is resposible for processing "events" form the UI.
So every event in the windows application must go through above method. SendKeys method is a special of SendMessage (MSDN), so you can use SendMessage to control other exe than your.
Simple code could look like:
IntPtr hwnd = FindWindow("Notepad++", null);
SendMessageA(hwnd, WM_COMMAND, SOMETHING1, SOMETHING2);
There is already on StackOverflow example how to do that with chrome: C# - Sending messages to Google Chrome from C# application , but this is only a start. You will have to find out what exactly message you want to send.
In exactly situation which you described I will try to send WM_MOUSE and WM_KEYBORD events to Notepad++ events, but it is only an idea :)
I work on Outlook addin that is connected to an Owncloud (Caldav, Cardav).
I use eventhandlers to detect when a user delete, create or update a contact item.
In this case this will notify the server and do an update on his side.
storage.Aitems[storage.Aitems.Count - 1].ItemAdd += new Outlook.ItemsEvents_ItemAddEventHandler(contact_item_add);
storage.Aitems[storage.Aitems.Count - 1].ItemChange += new Outlook.ItemsEvents_ItemChangeEventHandler(contact_item_change);
((MAPIFolderEvents_12_Event)storage.Afolders[storage.Afolders.Count - 1]).BeforeItemMove += new MAPIFolderEvents_12_BeforeItemMoveEventHandler(contact_item_before_move);
When i find new contacts items from the server i add them right the addressbook folder. Outlook detect a new item or a update and then fire the eventshandlers. This is good in case there is only one call (I can say this from the server dont notify again the server or this is from the user notify the server) but i can't because the events are fired multiple times.
static void contact_item_change(object Item) {
Microsoft.Office.Interop.Outlook.ContactItem contact = (Microsoft.Office.Interop.Outlook.ContactItem)Item;
System.Diagnostics.Debug.WriteLine("[Modification contact]" + contact.FullName);
// Need to know if item was created by code (server) or user
Main.SyncContact(contact);
}
Is it possible to know if an item is created trough the GUI or in my code ?
I can't set a variable to know if it was created by user or by my code because of the multiple calls of events.
BTW i already succeed to fire add and delete only one time :
static void contact_item_add(object Item) {
Microsoft.Office.Interop.Outlook.ContactItem contact = (Microsoft.Office.Interop.Outlook.ContactItem)Item;
if (Contact.GetCreatedByUserProperty(contact, "USER")) {
if (storage.PreviousAddedContactId != contact.EntryID)
{
System.Diagnostics.Debug.WriteLine("[Ajout contact]" + contact.FullName);
Main.SyncContact(contact);
storage.PreviousAddedContactId = contact.EntryID;
}
}
}
static void contact_item_before_move(object item, MAPIFolder destinationFolder, ref bool cancel) {
if ((destinationFolder == null) || (IsDeletedItemsFolder(destinationFolder))) {
ContactItem contact = (item as ContactItem);
if (storage.PreviousDeletedContactId != contact.EntryID)
{
System.Diagnostics.Debug.WriteLine("[Suppression du contact]" + contact.FullName);
Main.DeleteContact(contact);
storage.PreviousDeletedContactId = contact.EntryID;
}
}
}
Thank you !
You can read MailItem.LastModificationTime immediately after calling MailItem.Save. When ItemChange event fires, you can compare the new modification time with what you cached and check if it is greater than some delta (1 second?).
In case of Exchange store, you can also retrieve PR_CHANGE_KEY (DASL name http://schemas.microsoft.com/mapi/proptag/0x65E20102) property - it will change after each modification.
Is it possible to know if an item is created trough the GUI or in my code ?
No, there is no property or method which can help in identifying the way which was used to create an Outlook item. But you may add a user property which may indicate that the item was created by your code programmatically. User created item will not have this property set.
I followed the answer of Dmitry Streblechenko
In fact ContactItem have a LastModificationTime property. I don't know if i'm wrong but i guess outlook never change this Datetime when he makes changes. So it's easy to check if user have touch the contact. That allow me to notify the server and it works very well ! Thank you !
static void contact_item_change(object Item) {
Microsoft.Office.Interop.Outlook.ContactItem contact = (Microsoft.Office.Interop.Outlook.ContactItem)Item;
// Need to know if item was created by code (server) or user
var seconds = (DateTime.Now - contact.LastModificationTime).TotalSeconds;
if (seconds < 2) {
System.Diagnostics.Debug.WriteLine("[Modification contact]" + contact.FullName);
Main.SyncContact(contact);
}
}
You only need to focus on properties you want on that Item. When ItemChange event
fired, you can compare properties of that Item with your server Item, if they are difference => do Main.SyncContact(contact);