Wyobraźcie sobie sytuacje gdy jesteście poproszeni przez szefa by za każdym razem gdy ankieta zostanie wypełniona (skończona) został wysłany mail do działu HR z informacją, że nowa/zmodyfikowana ankieta została zapisana na listę.
Wydawało by się, że to nic prostszego :) Dodanie workflow lub SPItemEventReceiverItemAdded/ItemUpdated i wykonanie odpowiedniej operacji. Nic bardziej mylnego :)
Workflowy na ankietach nie działają i działać nie będą. KB opisujący problem znajdziecie tutaj. Jednak jak wiadomo MS czasami pisze różne rzeczy (Vista szybsza i mniej wymagająca od XP ;) itp.). Więc po pewnych analizach .NET Reflector doszedłem do następujących wniosków.
Podczas startu workflow, SharePoint wywołuje następującą metodę: Microsoft.SharePoint.Workflow.SPWinOEWSSService.MakeActivation, która, wywołuje konstruktor klasy: Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties, który zaś próbuje pobrać kolumnę na liście o ID ustawionym w zmiennej statycznej: Microsoft.SharePoint.SPBuiltInFieldId.GUID. Tej kolumny na liście Survey nie ma, dlatego też workflow na Survey nigdy nie zadziała, i nie sądzę, żeby patche do SharePoint to zmieniły.
Zaś ItemAdded i ItemUpdated będą działać bardzo dobrze na ankietach, które nie posiadają Page Separators. Czyli cała ankieta mieści się na jednej stronie, i od razu widzimy przycisk Finish :)
Niestety świat nie jest piękny – przynajmniej aż tak ;) Wystarczy, że pojawi się Page Separators a już przy akcji Next zostanie wywołane zdarzenie ItemAdded, każde następne Next oraz Finishspowodują wywołanie ItemUpdated. Wyobrażacie sobie minę działu HR który z jednej ankiety otrzymuje 20 maili? ;)
A więc do dzieła :) Każda ankieta zawiera kolumnę Completed:
Która określa stan wypełnienia ankiety. Jeżeli kolumna przyjmuje wartość Yes oznacza, że ankieta została w wypełniona i zapisana. Status No pojawia się wtedy gdy ankieta zostanie zapisana ale nie skończona, lub jest w trakcie wypełniania (pamiętacie o Page Separators i ItemAdded?):
Jakbym teraz klikną na Cancel element zostałby usunięty z listy zaś następny przyjąłby wartość #62:
By było jeszcze ciekawiej, ankieta zawiera pole Completed, która pryzmuje wartości 1 i 255 :) i skąd biedny człowiek ma wiedzieć co one oznaczają? Problem polega na tym, że odczyt tylko tej kolumny nie załatwi sprawy, ba nawet Microsoft z niej nie korzysta :) Skąd wiem? Otwórzcie SharePoint Designera i stronę AllItems.aspx dla ankiety. Następnie prawym przyciskiem myszy kliknijcie na Web Part odpowiedzialny z wyświetlenie listy i wybierzcie opcję:
Następnie w kodzie wyszukajcie tekstu: 255, powinniście zobaczyć coś takiego:
I dochodzi nam kolejna kolumna _Level :) i obiecuje wam, ze nie ostatnia :) ale reszta będzie opisana w kodzie.
Prosta spraw zamieniła się w dość skomplikowaną :) ale nie ma rzeczy nie możliwych ;) Więc zacznijmy tworzyć SPItemEventReceiver z… metodami ItemAdded i ItemUpdating (mała różnica ale znacząca!) :)
Szkielet naszej klasy powinien wyglądać mniej więcej tak:
public class SurveyItemEventReceiver : SPItemEventReceiver { [SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")] [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)] public override void ItemAdded(SPItemEventProperties properties) { } [SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")] [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)] public override void ItemUpdating(SPItemEventProperties properties) { } }
Zacznijmy implementację od ItemAdded:
[SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")] [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)] public override void ItemAdded(SPItemEventProperties properties) { // ItemAdded jest nam potrzebne tylko i wylacznie wtedy kiedy ankieta sklada sie // z jednej strony, w przeciwnym wypadku, wszystkie operacje bedziemy wykonywac // na ItemUpdated. // Pobranie poziomu - zgodnie z tym co twierdzi SharePoint oraz ShrePoint Designer // to wlasnie ta wartosc okresla czy ankieta zostala skonczona czy tez nie. // Wartosc _Level jest nazwa wewnetrzna (Internal Name), jako ze dzialamy na // asynchronicznym zdarzeniu, wiemy juz ze element zostal dodany do listy // przez co properties.ListItem bedzie zawieralo jego wlasnosci. int level = Convert.ToInt32(properties.ListItem["Level"]); // Tak jak wam obiecalem, kolejne pole :) Jednakze ma ono kluczowe znaczenie. // To pole pojawia sie tylko i wylacznie wtedy, gdy nastepuje ostatnie // zapisanie/zaktualizowanie ankiety. To znaczy, ze jezeli mamy przycisk // Next to pole sie nie pojawi! zas jezeli mamy przycisk Finish, to pole // sie pojawi! :) To co nas tak naprawde interesuje to, to czy ono istnieje // jezeli nie istnieje to znaczy ze to nie koniec :) object ctype = properties.AfterProperties["ContentType"]; // Teraz kluczowy element. Wiemy juz ze 255 oznacza nie skonczona ankieta. Wiemy // takze ze dzialamy w asynchronicznym zdarzeniu po fizycznym dodaniu elementu do listy. // Czyli level juz ulegl zmianie na 1 jezeli naprawde zakonczylismy dodwanie elementu. // W tym wypadku ctype jest jedynie wetylem bezpieczenstwa - po tym co widzialem w // SharePoint moge sie spodziewac wszystkiego, wiec wole sie ubezpieczyc. if (level != 255 && ctype != null) { // A tutaj slemy e-mail SPUtility.SendEmail( properties.OpenWeb(), false, false, "[email protected]", "Nowa ankieta wgrana przez " + properties.UserLoginName, "Zapraszamy do obejrzenia" ); } }
Komentarze w kodzie powinny wszystko wyjaśnić, jeżeli nie to zapraszam do kontaktu za pomocą komentarzy.
Dobrze, mamy połowę roboty z głowy, teraz pora zająć się ItemUpdating. Z nim mamy dwa problemy, po pierwsze ItemAdded działa wtedy i tylko wtedy gdy od razu mamy przycisk Finish, czyli obsługujemy tylko jeden przypadek, zaś w ItemUpdating mamy dwa przypadki, ankieta nie została jeszcze w ogóle dodana i ankieta została dodana i teraz jest aktualizowana.
Implementacja ItemUpdating powinna wyglądać następująco:
[SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")] [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)] public override void ItemUpdating(SPItemEventProperties properties) { // ItemUpdating jest nam potrzebne w kazdym miejscu! oprocz kiedy nastepuje // przypadek ItemAdded - czyli niednostronnicowa ankieta. // Pobranie poziomu - zgodnie z tym co twierdzi SharePoint oraz ShrePoint Designer // to wlasnie ta wartosc okresla czy ankieta zostala skonczona czy tez nie. // Wartosc _Level jest nazwa wewnetrzna (Internal Name), jako ze dzialamy na // synchronicznym zdarzeniu ItemUpdating, wiemy juz ze element zostal dodany do listy // przez co properties.ListItem bedzie zawieralo jego wlasnosci. int level = Convert.ToInt32(properties.ListItem["Level"]); // Tak jak wam obiecalem, kolejne pole :) Jednakze ma ono kluczowe znaczenie. // To pole pojawia sie tylko i wylacznie wtedy, gdy nastepuje ostatnie // zapisanie/zaktualizowanie ankiety. To znaczy, ze jezeli mamy przycisk // Next to pole sie nie pojawi! zas jezeli mamy przycisk Finish, to pole // sie pojawi! :) To co nas tak naprawde interesuje to, to czy ono istnieje // jezeli nie istnieje to znaczy ze to nie koniec :) object ctype = properties.AfterProperties["ContentType"]; // Jezeli aktualnie nie bylismy na ostatniej stronie (ctype) // oraz wciaz nie skonczylismy ankiety (level) to kontynuujmy // dalej jej wypelnianie if (level == 255 && ctype == null) { // Kontynuacja wypelniania return; } // Jezeli aktualnie jestesmy na ostatnie stronie (ctype) // oraz aktualnie zapisana ankieta nie jest jescze skonczona (level) // to oznacza ze wypelnilismy nowa ankiete. Dzieje sie tak, gdyz // level zawiera wartosc przed aktualizacja/zapisem ankiety na liscie // czyli dopiero po operacji ItemUpdating przyjmi on wartosc 1. else if (level == 255 && ctype != null) { // A tutaj slemy e-mail SPUtility.SendEmail( properties.OpenWeb(), false, false, "[email protected]", "Nowa ankieta wgrana przez " + properties.UserLoginName, "Zapraszamy do obejrzenia" ); } // Jezeli ankieta zostala juz zapisana i teraz ja edytujemy (level) // i wciaz nie jestesmy na ostatniej stronie (ctype) to kontynuujmy // wypelnianie ankiety else if (level != 255 && ctype == null) { // Kontynuacja wypelniania return; } // Jezeli ankieta zostala juz zapisana i teraz ja edytujemy (level) // i jestesmy wlasnie na ostatniej stronie (ctype) to wyslijmy maila // do HR! else if (level != 255 && ctype != null) { // A tutaj slemy e-mail SPUtility.SendEmail( properties.OpenWeb(), false, false, "[email protected]", "Zaktualizowana ankieta wgrana przez " + properties.UserLoginName, "Zapraszamy do obejrzenia" ); } }
Dla wyjaśnienia. W tym wypadku działamy na zdarzeniu synchronicznym. Zdarzenie to wywoływane jest tuż przed zapisaniem zmian elementu na listę. Dlatego wartości w ListItem są stare (te przed aktualizacją, zaś wartości w AfterProperties są wartościami zmienionymi – i tylko wyłącznie zmienionymi, to znaczy tymi które uległy zmianie). Nie odwołuje się do BeforePropertiesze względu na to, że nie byłbym wstanie wziąć poziomu z jego własności, dostał bym wartość null.
I to wszystko! Teraz należy tylko stworzyć feature.xml i wgrać nasz SPItemEventReceiver. Możemy to też zrobić z poziomu kodu. Jednakże zakładam, że wiecie jak to zrobić :) Jak nie, zapraszam do komentarzy, postaram się odpowiedzieć na wszystkie pytania a jeżeli będą się one powatarzały to stworzę wpis na blogu wyjaśniający wątpliwości.