Jedną z podstawowych zasad w Office dev którą trzeba przestrzegać a które nie jest nigdzie jasno pogrubiona to informacja o tym co robić z obiektami officowymi. Jest to też rzecz, która prowadzi do problemów które są bardzo ciężkie do wykrycia i które mogą omylnie wskazywać na zupełnie inny problem. Na przykład u mnie nie można było zrobić reply/forward na mailu ale tylko i wyłącznie w jednej, konkretnej sytuacji – co najlepsze mój plugin nie miał nic wspólnego z wysyłaniem maili. Czemu akurat tej? Nie wiem. Ale wiem co naprawiło problem.
Zasada w office jest dość prosta, wszystkie obiekty które my tworzymy/rzutujemy powinny być przez nas zwalniane – oprócz kolekcji (powtarzam mądrości przeczytane, sam nie wiem czemu). Co to znaczy zwalniane? W .NET mamy metodę:
Marshal.ReleaseComObject(object);
Którą trzeba wykonać na obiekcie, który jest nie jest null i który jest referencją do obiektu COM (ogólnie chodzi o zmniejszanie liczbę referencji do obiektu w RCW
– Runtime Callable Wrapper), ogólnie coś COMowego, jak nie potrzebujecie wiedzieć co to to tym lepiej! Dobrym zwyczajem jest też przypisanie obiektu do null, ale to już nie jest takie konieczne. Taką metodę można łatwo opakować albo w generyczny kod albo bardziej precyzyjny – u mnie przy 70 typach, lepiej było to zrobić generycznie:
public static class OfficeObjectExtensions { public static void Release(dynamic @this) { internal_release((object)@this); } public static void Release<T>(this T @this) { internal_release(@this); } private static void internal_release(object obj) { if (obj == null) { return; } Marshal.ReleaseComObject(obj); } }
Można z niej skorzystać w następujący sposób:
var mail = inspector.CurrentItem as _MailItem; mail.Release();
Czyli idąc zasadą już opisaną, jeżeli mamy zdarzenie które przyjmuje jakiś obiekt:
void ThisWorkbook_SheetChange(object ws, Excel.Range rng) { var workSheet = ws as Excel.Worksheet; }
To nawet jak rzutujemy to na obiekt office to nie zwalniamy go. Ale jeżeli jednak robimy już coś w stylu:
private void OnNewInspector(Outlook.Inspector inspector) { var mail = inspector.CurrentItem as Outlook._MailItem; mail.Release(); }
To obiekt powinien być zwolniony.
Zasada jest banalna i łatwo ją przestrzegać. Na przykład ja zrobiłem tak, że kaskadowo przekazywałem obiekt _MailItem
do około 5-6 metod, ale tylko jedna była odpowiedzialna za zarządzanie nim. Reszta to wykorzystywała.
Dzięki czemu było mi łatwo znaleźć miejsca w których zapomniałem o Release
.
Zasada ta też tyczy wszystkich iteracji typu:
foreach(var x in mail.Properties) { x.Release(); }
Jak i zwykłego for. Jedynym wyjątkiem od reguły jest sytuacja, kiedy autor biblioteki/rozszerzenia informuje nas o możliwości zwolnienia z pamięci naszego obiektu. Na przykład jest metoda podłącz zdarzenia do elementu która ma opcję: zwolnij zasoby przy odłączeniu zdarzeń od elementu. Wtedy możemy zdać się na twórców jej, że rzeczy zostaną zwolnione.
Podsumowanie
Z tym nie ma żartuj, poniżej zamieszam trochę dłuższy opis problemów na jakie ja natrafiłem w trakcie pisania rozszerzenia. Tak by zobrazować co jakiego typu błędy mogą myć spowodowane przez złe zwalnianie obiektów – zbyt szybkie, późne lub brak.
Opis dla chętnych problemów i tych którzy nie lubią TL;DR
Ostatnio przez 2 tygodnie walczyłem z jednym błędem w outlooku, który o dziwo pojawiał się tylko w jednym konkretnym przypadku – dosłownie. Sytuacja jest u mnie prosta, mam 4 przyciski, każdy robi to samo: ustawia właściwości maila (mail properties, nic co jest widoczne ani przekazywane dalej, taki lokalny storage), dodaje tekst na początku maila. Na otwarciu maila, jeżeli jest to nowy mail, zaznaczany domyślnie jest pierwszy przycisk a jeżeli jest to odpowiedź czy forward, to jest zaznaczany odpowiedni przycisk od pierwszego do czwartego. Kod zaznaczania przycisków jest taki sam. Ale w jednym przypadku to nie działało. Dosłownie w jednym.
Aplikacja nie działała, kiedy próbowało się zrobić forward/reply maila w formacie RTF z załącznikiem typu OLE dla jednego (tylko jednego!) przycisku. Dla innych rodzajów załączników jak i formatów maila problem absolutnie nie istniał zaś kliknięcie na wszystkie pozostałe przyciski przy RTF i OLE powodowało, że mail działał tak jak powinien. Tylko w tym jednym przypadku powodował błąd synchronizacji! z exchange:
12:08:53 Synchronizer Version 12.0.6672 12:08:53 Synchronizing Mailbox 'Gutkowski, Jakub *****’ 12:08:53 Synchronizing local changes in folder ‘******’ 12:08:53 Uploading to server ‘*****.******.*******’ 12:08:53 Error synchronizing message 'FW: ' 12:08:53 [8004011B-3EE-8004011B-324] 12:08:53 Unknown Error. 12:08:53 Microsoft Exchange Information Store 12:08:53 For more information on this failure, click the URL below: 12:08:53 http://www.microsoft.com/support/prodredirect/outlook2000_us.asp?err=8004011b-3ee-8004011b-324 12:08:53 Moved a message that failed synchronization to ‘****’. Message subject -> 'FW: '. You can view this message in your offline folder file only. 12:08:54 Done
Który był schowanym błędem synchronizacji problemu poniższego:
The following recipient(s) could not be reached: gutkowski@****.***** on 16/06/2007 9:49 AM This message could not be sent. Try sending the message again later, or contact your network administrator. Error is [0x80004005-00000000-00000000].
Mój plugin nie ma nic wspólnego z wysłaniem maili czy z synchronizacją itp. jedynie modyfikuje dwie wartości i koniec. Błąd się pojawiał po wysłaniu. Opisy w sieci, że to profil outlooka jest popsuty nie sprawdziły się. Najlepsze było to, że:
- Maila nie było w wysłanych ani w draft (zniknął)
- Była opcja wysłania maila ponownie z okna w którym powyższy błąd się pojawił
- Próba wysłania ponownego maila pokazywała puste okno – bez treści, cały mail zniknął.
Przy większej analizie wychodziło na to, że zapis draft maila z klikniętym felernym przyciskiem też poprawnie nie działał – mail znikał przy zapisywaniu. Wystarczyło zmienić przycisk i wszystko było ok.
I tak, 50 razy sprawdzałem, ten sam kod jest wykonywany przy każdym z tych przycisków. No jak się okazało, prawie ten sam – a okazało się tak, bo inaczej nie jestem wstanie odpowiedzieć na pytanie, zaś przejście ponowne przez wszystkie Release
i zastosowanie się do zasady wymienionej wyżej, naprawiło problem.
Czyli jak widać… coś super błahego doprowadziło do niesamowitych problemów których raczej nikt by się nie spodziewał.