From what I understand you need to track Activation and Deactivation of the Explorers. During activation, you need to add SelectionChange event handlers for the current explorer.
This seems to work perfectly for single clicks on AppointmentItems. But it crashes the Addin when double-clicking on an appointment series and selecting a single Appointment.
Here is the source:
On class level
private Outlook.Explorer currentExplorer = null;
private Outlook.AppointmentItem currentAppointmentItem = null;
within Startup:
currentExplorer = this.Application.ActiveExplorer();
((Outlook.ExplorerEvents_10_Event)currentExplorer).Activate +=
new Outlook.ExplorerEvents_10_ActivateEventHandler(
Explorer_Activate);
currentExplorer.Deactivate += new
Outlook.ExplorerEvents_10_DeactivateEventHandler(
Explorer_Deactivate);
The event handlers:
void Explorer_Activate()
{
currentExplorer.SelectionChange += new Outlook.ExplorerEvents_10_SelectionChangeEventHandler(Selection_Change);
}
void Explorer_Deactivate()
{
currentExplorer.SelectionChange -= new Outlook.ExplorerEvents_10_SelectionChangeEventHandler(Selection_Change); ;
}
private void Close_Explorer()
{
}
private void Selection_Change()
{
Outlook.MAPIFolder selectedFolder = currentExplorer.CurrentFolder;
if (currentExplorer.Selection.Count > 0)
{
Object selObject = currentExplorer.Selection[1];
if (selObject is Outlook.AppointmentItem)
{
currentAppointmentItem = (Outlook.AppointmentItem)selObject;
}
else
{
currentAppointmentItem = null;
}
}
}
What am I overlooking? Is the form of deregistering a problem?
Try to add try/catch blocks to the event handlers. The Outlook object model can give you unpredictable results sometimes. It is worth adding them and find where an exception is thrown.
currentExplorer.Selection.Count
Also, you may subscribe to the SelectionChange event in the NewExplorer event and don't switch between explorers when they are activated or deactivated. The event is fired whenever a new explorer window is opened, either as a result of user action or through program code.
The only thing which I added was a handler for NewInspector and InspectorClose events along with Marshal.ReleaseComObject(). The only thing which I can imagine that double clicking while debugging I got in some kind of race condition (because double clicking also triggers the Selection_Change event). But this is only a guess.
You do not need to add and remove event handlers as an explorer is activated / deactivated. Are you trying to support multiple explorers? In that case, create a wrapper class that hold the Explorer object as it member and uses its methods as event handlers.
Related
What I am trying to achieve is to also handle drag and drop events on the calendar properly using the AppProperty Change event on the Inspector:
I update the currentAppointmentItem whenever the user interacts with the interface (Explorer.SelectionChange, NewInspector, CloseInspector, etc.)
I update the currentInspector whenever the user interacts with the interface (SelectionChange, NewInspector, CloseInspector)
Update means that I try to set/unset the event handlers appropriately and to Marshal.ReleaseComObject accordingly. Finally to null the reference.
But when the user just clicks on an AppointmentItem in the calendar no Inspector window is created. Thus I wouldn't be able to catch AppPropertyChange Events. So I decided to call GetInspector on the selected AppointmentItem in case it is not null. I try to use this to receive changes on the AppProperty Event so I can handle drag and drop events on the calendar properly
The problem: From the Microsoft documentation I understand whenever you lose a reference to currentAppointmentItem you should also use Marshal.ReleaseComObject otherwise you risk other problems.
Now I experience exceptions which I cannot catch: RaceOnRCWCleanup ... it seems that I try to release a COM object which is still in use (probably by Outlook). How can I avoid that? Is it correct to Marshal.ReleaseComObject(currentAppointmentItem)
I registered on the SelectionChange Event on the Outlook.Explorer. In there I try to register the currentAppointment with:
[...]
log.Info("Selection_Change");
if (currentExplorer == null)
{
return;
}
try
{
log.Info("Selection_Change: " + currentExplorer.Caption);
Outlook.MAPIFolder selectedFolder = currentExplorer.CurrentFolder;
if (currentExplorer.Selection.Count > 0)
{
Object selObject = currentExplorer.Selection[1];
if (selObject is Outlook.AppointmentItem)
{
currentAppointmentItem = (Outlook.AppointmentItem)selObject;
Inspectors_NewInspector(currentAppointmentItem.GetInspector);
}
[...]
Please Note: INspectors_NewInspector is also called on the Inspectors Collection.
The code of NewInspector is like
void Inspectors_NewInspector(Microsoft.Office.Interop.Outlook.Inspector Inspector)
{
try
{
log.Info("Inspectors_NewInspector");
// This function (apparently) gets kicked off whenever a user opens a new or existing item
// in Outlook (Calendar appointment, Email, etc).
// We can intercept it, modify it's properties, before letting our Ribbon know about its existance.
//
if (Inspector != null)
{
log.Info("Inspectors_NewInspector: " + Inspector.Caption);
unregisterCurrentInspector();
currentInspector = Inspector;
object item = Inspector.CurrentItem;
if (item == null)
return;
if (!(item is Outlook.AppointmentItem))
return;
unregisterCurrentAppointmentItem();
currentAppointmentItem = (Outlook.AppointmentItem)item;
currentAppointmentItem.PropertyChange += AppPropertyChanged; // Handle situations where the
// user tries to convert an appointment w/ an agreedo protocol to a recurring appointment.
// This needs to be avoided .
currentAppointmentItem.CustomPropertyChange += AppPropertyChanged;
}
((Microsoft.Office.Interop.Outlook.InspectorEvents_10_Event)Inspector).Close += Inspector_Close;
} catch (Exception ex)
{
log.Error(ex.Message);
}
}
the unregisterCurrentApppointmentItem :
private void unregisterCurrentAppointmentItem()
{
try
{
log.Info("unregisterCurrentAppointmentItem");
if (currentAppointmentItem != null)
{
currentAppointmentItem.PropertyChange -= AppPropertyChanged; // Handle situations where the
currentAppointmentItem.CustomPropertyChange -= AppPropertyChanged;
Marshal.ReleaseComObject(currentAppointmentItem);
currentAppointmentItem = null;
}
} catch (Exception ex)
{
log.Error(ex.Message);
}
}
the unregisterCurrentInspector:
private void unregisterCurrentInspector()
{
log.Info("unregisterCurrentInspector");
if (currentInspector != null)
{
((Microsoft.Office.Interop.Outlook.InspectorEvents_10_Event)currentInspector).Close -= Inspector_Close;
Marshal.ReleaseComObject(currentInspector);
currentInspector = null;
}
}
Any advice on this?
What I already tried / taken into account:
Outlook Addin: Moving Appointment in Calendar does not reflect new date/time in AppointmentItem (catch Calendar.ItemChange)
VSTO Outlook Plugin: Cannot get AppointmentItem in Item_Change event when recurring appointment is dragged and dropped by user
First of all, there is no need to simulate the NewInspector event. Instead, you need to set up event handlers correctly. It seems you just need to implement an inspector or explorer wrappers. See Implement a wrapper for inspectors and track item-level events in each inspector for more information.
it seems that I try to release a COM object which is still in use (probably by Outlook). How can I avoid that? Is it correct to Marshal.ReleaseComObject(currentAppointmentItem)
Yes, it is. But you should really use this method against objects retrieved in your code by calling properties and methods. You SHOULD NOT release objects passed as parameters by the Office applications. Take a look a the When to release COM objects in Office add-ins developed in .NET article which explains possible pitfalls and give answers to the most widely spread questions.
Why do you even need the Inspector object? Are you only using the Inspector.Close event?
Use AppointmentItem.Close / Write events.
I have a Xamarin form map on my screen and I'm using PropertyChanged event to retrieve geolocation information from my server and display the proper pins on screen.
While coding the solution I noticed the PropertyChanged event is triggered multiple times (up to 10 times) with a single zoom or drag action on the map. This causes unnecessary calls to server which I want to avoid.
Ideally I want to make only one call to server when the final PropertyChanged event is called but I cant's find an easy solution to implement this.
At this point I've added a refresh button to my page that becomes enabled when a PropertyChanged event happens and I disable it after user uses the button.
Obviously this fixed the too many calls to server but made the solution manual.
I was wondering if there is a more elegant way to make the server call but do it automatically.
Thanks in advance.
I just test the PropertyChanged event on iOS side and it just triggered one time with a single zoom or drag action on the map.
While if it really triggered multiple times, you can use a timer to call the server when the final PropertyChanged event is called, for example:
public partial class MapPage : ContentPage
{
Timer aTimer;
public MapPage()
{
InitializeComponent();
customMap.PropertyChanged += CustomMap_PropertyChanged;
}
private void CustomMap_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (aTimer != null)
{
aTimer.Enabled = false;
aTimer.Stop();
aTimer.Close();
}
aTimer = new Timer();
aTimer.Interval = 1000;
aTimer.Enabled = true;
aTimer.Elapsed += ATimer_Elapsed;
aTimer.Start();
}
private void ATimer_Elapsed(object sender, ElapsedEventArgs e)
{
aTimer.Stop();
//do web request
Console.WriteLine(sender);
Console.WriteLine("CustomMap_PropertyChanged");
}
}
In the above code, I set the Interval = 1 second, that means in 1 second, whatever how many times PropertyChanged triggered, only the last call will trigger the ATimer_Elapsed function.
The Interval can be set to any value depending on your requirement.
On my development machine I have the FormInitializing and FormShowing events firing before RibbonLoad. I created a setup package in VS 2010 and installed on a vanilla Windows 7 Ultimate with Outlook 2010 installed.
The addin wasn't appearing on my meeting request form. So I setup remote debugger and to my astonishment the RibbonLoad is firing before the two form events mentioned above. A null exception is being throw b\c the code in the RibbonLoad relies on the FormRegion already being loaded. Can anyone offer any insight?
There is no defined order for certain Outlook events - the Ribbon UI and the Inspector UI are completely different components, despite them both being display in the same window. The Outlook runtime may trigger Ribbon and Inspector events in different orders. It would be your job to synchronize the two events (RibbonLoad and FormInitializing) if you need some initialization done. You cannot assume that the ordering will always be the same.
I notice this same behavior when ThisAddIn.Startup fires before ThisAddIn.CreateRibbonExtensibilityObject, but sometimes after depending on how Outlook triggers the sequencing. You can just use a static variable with sync locking to ensure your initialization code is only triggered once.
Here is an example I used to synchronize the Startup event with the RibbonLoad event:
public partial class ThisAddIn
{
static bool formInitialized = false;
static readonly object padLock = new Object();
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
lock(padLock) { if (!formInitialized ) { InitializeForm(); } }
// startup code
}
private void InitializeForm()
{
// init code
formInitialized = true;
}
protected override IRibbonExtensibility CreateRibbonExtensibilityObject()
{
lock(padLock) { if (!formInitialized) InitializeForm(); }
// Create ribbon UI
}
}
I'm working on a WPF application that monitors numerous folders in an Outlook Shared Mailbox. I have wired up ItemAdd and ItemRemove event handlers to a Folder.Items object.
Everything works great for a few minutes. But as time goes on, the event handling seems to go "poof". Some folders will still recognize add and remove, others will only see removes, while others are blind to any activity. To me it seems like the event handlers are being garbage collected, but my Items object IS declared as a global variable in the class it sits in, so I don't see how they could be GC'd out.
Are there any pitfalls I should be aware of with Outlook Folder.Items events? I have a previous, simpler application that works by similar processes that works fine for extended periods of time. There is no intrinsic difference, as far as Item event handling goes, between my old app and this new one. I'm really at a loss as to what's causing this.
Below is the relevant code. To bring some context to this, what I'm doing is, for each Folder in the Outlook Shared Mailbox a "TicketView" UserControl is created which represents the contents (MailItems) of that folder. This TicketView is a simple ListBox that may contain between 0 to a couple dozen MailItems -- nothing excessive.
public partial class TicketView : UserControl
{
private Folder _thisFolder = null;
private TicketCollection _thisTicketColl = null;
private Items _thisItems = null;
public TicketView(Folder folder)
{
InitializeComponent();
_thisTicketColl = this.FindResource("TicketCollection") as TicketCollection;
_thisFolder = folder;
_thisItems = folder.Items;
SetFolderEvents();
Refresh();
}
private void SetFolderEvents()
{
_thisItems.ItemAdd += new ItemsEvents_ItemAddEventHandler(delegate
{
Refresh();
});
_thisItems.ItemRemove += new ItemsEvents_ItemRemoveEventHandler(delegate
{
Refresh();
});
}
public void Refresh()
{
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += new DoWorkEventHandler(delegate(object sender, DoWorkEventArgs e)
{
string[] fields = new string[] { "Subject", "SenderName", "SentOn", "EntryID" };
var olTable = TicketMonitorStatics.GetOutlookTable(_thisFolder, fields, filter);
olTable.Sort("SentOn", true);
var refreshedList = new List<Ticket>();
while (!olTable.EndOfTable)
{
var olRow = olTable.GetNextRow();
refreshedList.Add(new Ticket
{
Subject = olRow["Subject"],
Sender = olRow["SenderName"],
SentOn = olRow["SentOn"],
EntryID = olRow["EntryID"]
});
};
e.Result = refreshedList;
});
worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(delegate(object sender, RunWorkerCompletedEventArgs e)
{
var refreshedList = e.Result as List<Ticket>;
UpdateTicketList(refreshedList);
worker.Dispose();
});
worker.RunWorkerAsync();
}
private void UpdateTicketList(List<Ticket> newList)
{
_thisTicketColl.Clear();
foreach (Ticket t in newList)
{
_thisTicketColl.Add(t);
}
}
}
}
Outlook events should not be used for any kind of synchronization. They are designed to be used for the UI purposes only and can be dropped under heavy loads or if a network error occurs (if you are using an online store).
You can use events only as a hint that your code needs to run sooner rather than later.
You can use the IExchangeExportChanges MAPI interface (C++ or Delphi only) to perform synchronization; this is the same API used by Outlook to synchronize its cached folders. If you are not using C++ or Delphi, you can use Redemption (I am its author) and its RDOFolderSynchronizer object.
I've made an extension inside a package and I am calling the following code (occurs when a user presses a button in the toolbar):
DocumentEvents documentEvents = (DTE2)GetService(typeof(DTE));
_dte.Events.DebuggerEvents.OnEnterBreakMode += DebuggerEvents_OnEnterBreakMode;
_dte.Events.DebuggerEvents.OnEnterDesignMode += DebuggerEvents_OnEnterDesignMode;
_dte.Events.DebuggerEvents.OnContextChanged += DebuggerEvents_OnContextChanged;
_dte.Events.DocumentEvents.DocumentSaved += new _dispDocumentEvents_DocumentSavedEventHandler(DocumentEvents_DocumentSaved);
_dte.Events.DocumentEvents.DocumentOpened += new _dispDocumentEvents_DocumentOpenedEventHandler(DocumentEvents_DocumentOpened);
void DocumentEvents_DocumentOpened(Document Document)
{
}
void DocumentEvents_DocumentSaved(Document Document)
{
}
void DebuggerEvents_OnEnterBreakMode(dbgEventReason Reason, ref dbgExecutionAction ExecutionAction)
{
}
void DebuggerEvents_OnContextChanged(Process NewProcess, Program NewProgram, Thread NewThread, StackFrame NewStackFrame)
{
}
private void DebuggerEvents_OnEnterDesignMode(dbgEventReason reason)
{
}
The first and the major problem is that the subscription to the event doesn't work. I've tried:
Opening new documents
Detaching from debug (thus supposedly triggering OnEnterDesignMode
Saving a document
None of these seem to have any effect and the callback functions were never called.
The second issue is that the subscription to the event line works USUALLY (the subscription itself, the callback doesn't work as described above) but after a while running the subscription line, e.g:
_dte.Events.DebuggerEvents.OnEnterBreakMode -= DebuggerEvents_OnEnterBreakMode;
Causes an exception:
Exception occured!
System.Runtime.InteropServices.InvalidComObjectException: COM object that has been separated from its underlying RCW cannot be used.
at System.StubHelpers.StubHelpers.StubRegisterRCW(Object pThis, IntPtr pThread)
at System.Runtime.InteropServices.UCOMIConnectionPoint.Unadvise(Int32 dwCookie)
at EnvDTE._dispDebuggerEvents_EventProvider.remove_OnEnterDesignMode(_dispDebuggerEvents_OnEnterDesignModeEventHandler A_1)
Any ideas will be welcome
Thanks!
Vitaly
Posting an answer that I got from MSDN forums, by Ryan Molden, in case it helps anyone:
I believe the problem here is how the
CLR handles COM endpoints (event
sinks). If I recall correctly when
you hit the
_applicationObject.Events.DebuggerEvents
part of your 'chain' the CLR will
create a NEW DebuggerEvents object for
the property access and WON'T cache
it, therefor it comes back to you, you
sign up an event handler to it (which
creates a strong ref between the
TEMPORARY object and your object due
to the delegate, but NOT from your
object to the temporary object, which
would prevent the GC). Then you don't
store that object anywhere so it is
immediately GC eligible and will
eventually be GC'ed.
I changed the code to store DebuggerEvents as a field and it all started to work fine.
Here is what #VitalyB means using code:
// list where we will place events.
// make sure that this variable is on global scope so that GC does not delete the evvents
List<object> events = new List<object>();
public void AddEvents(EnvDTE dte)
{
// create an event when a document is open
var docEvent = dte.Events.DocumentEvents;
// add event to list so that GC does not remove it
events.Add(docEvent );
docEvent.DocumentOpened += (document)=>{
Console.Write("document was opened!");
};
// you may add more events:
var commandEvent = dte.Events.CommandEvents;
events.Add(commandEvent );
commandEvent.AfterExecute+= etc...
}