Nie ma to jak dobry projekt w SharePoint, człowiek odkrywa ukryte funkcjonalności z dnia na dzień :) Ale do rzeczy, większość z was zna zapewne metodę Update() oraz SystemUpdate() na SPListItem. Różnica pomiędzy nimi jest dość prosta:
- Update() aktualizuje metadane, zmieniając informacje na temat daty modyfikacji i zwiększając wersję elementu jeżeli wersjonowanie jest włączone;
- SystemUpdate() aktualizuje metadane, nie zmieniając informacji na temat daty modyfikacji czy też wersji elementu.
Obie metody mają swoje plusy i minusy. Osobiście używam SystemUpdate() wszędzie tam gdzie serwer wykonuje operacje na elementach, zaś operacje użytkownika pozostawiam metodzie Update(). Wszystko było piękne aż do wczoraj (3 nad ranem), kiedy to nagle potrzebowałem trzeciego rodzaju metody aktualizującej element. Już tłumaczę o co chodzi :) W jednym z projektów, generuje faktury w PDF na podstawie wartości z kilku list. W tym celu wykorzystuje obiekt SPQuery na kilku listach z których następnie tworze odpowiednio z preparowane źródło danych. PDFy mogą być utworzone tylko i wyłącznie podczas zatwierdzania faktury, zaś zatwierdzanie modyfikuje kilka pól elementu tak by ustawić mu dane typu: Data Zatwierdzenia Faktury, czy Osoba Zatwierdzająca Fakturę, Status Faktury itp.
Wszystko działało wyśmienicie aż do wprowadzenia security na elementach. Mianowicie w zależności od pewnych pól w fakturze, podczas jej zatwierdzania osoby które są przypisane do wartości tych pól muszą mieć ustawione prawa dostępu do elementu, jak i prawa autora elementu muszą zostać zmienione. Przy tworzeniu rozwiązania nawet myślałem o tym by zrobić dwie listy i kopiować elementy by tylko w security nie wchodzić lub skończyć Security Manager dla List View – o którym mam nadzieję niebawem uda mi się napisać coś więcej ;) – dla osób nie korzystających z WSS, w WSS nie ma Audiences więc też nie ma jak przypisać View do pewnej grupy osób, albo View są nie widoczne, albo wszystkie View są widoczne, więc zarządzanie wyświetlaniem elementów poprzez View odpadło. Rozwiązanie na jakie się zdecydowałem to ustawianie bezpośrednio uprawnień na elementach podczas aktualizacji elementu.
Na początku chciałem to zrobić na zdarzenie ItemUpdating, jednak nie można wtedy aktualizować elementu, który właśnie jest aktualizowany – chyba, że się przepisze wszystkie pola i stworzy nowy element na liście na podstawie danych przekazanych do zdarzenia. Jest to czasochłonne, a przy tym można stracić pewne dane historyczne, ale da się to tak zrobić. Ja poszedłem na łatwiznę i zrobiłem ustawianie uprawnień na ItemUpdated.
Sytuacja która zaszła wyglądała tak:
- Użytkownik zatwierdza fakturę;
- Ja z kodu modyfikuje odpowiednie pola wywołuje Update na elemencie;
- Wywołuje generowanie PDFów i przypisanie ich do elementu;
Wszystko było by świetnie gdyby nie to, że ItemUpdated jest zdarzeniem asynchronicznym wywoływanym po aktualizacji elementu. W tym momencie mój punkt 3, czasami próbował zaktualizować element z SPContext.Current.ListItem który został zaktualizowany przez moje zdarzenie ItemUpdated – dodanie uprawnień wymaga aktualizacji elementu.
Przez to wszystko, PDFy się nie generowały – zwracały błąd Save Conflict – , zaś na stronie z której użytkownik zatwierdzał fakturę SPContext.Current.ListItem zwracał informacje, że je posiada PDFy. Dzieje się tak iż w danym kontekście element ma już te PDFy ma je tak długo do póki nie wyjdziemy ze strony lub nie wywołamy metody Update. Więc użytkownikowi pokazywał się błąd:Nie udało się wygenerować faktury w postaci PDF, na stronie pokazywały się linki do pobrania PDF, zaś ich kliknięcie zwracało błąd: Brak PDF do pobranie. Czyli maksymalny mis-masz. Dopiero odświeżenie całkowite strony pokazywało odpowiedni przycisk do generowania ponownego PDFów.
Zacząłem więc kombinować jak to zrobić by Update i ustawianie uprawnień działało i by PDFy się generowały. Update podczas zatwierdzania jednak był niezbędny ze względu na to iż do generowania PDFów wykorzystuje SPQuery, które zwraca elementy zapisane na listach. Więc bez Update zwrócił by niepoprawne dane. Zaś stworzenie PDFów wymaga zrobienia Update na elemencie gdyż inaczej załączniki nie zostaną zapisane. Można powiedzieć, że zapędziłem się w kozi róg :)
Jednak tym razem po tym jak przekląłem, wypiłem parę kaw oraz wypaliłem pół paczki, odpaliłem .NET Reflectora by przyjrzeć się jak MS wywołuje metodę Update i SystemUpdate() bo może da się coś zrobić by jednak te zdarzenia asynchroniczne nie były wywoływane kiedy nie chcę by były, zaś zostały wtedy kiedy o to SharePointa poproszę :)
W gwoli ścisłości, podczas pisania kodu w SPItemEventReceiver macie dostępne dwie metody:
- DisableEventFiring() – wyłącza wywoływanie zdarzeń przez co w ItemUpdate można wywołać Update na danym elemencie i zdarzenie nie zostanie ponownie wywołane :)
- EnableEventFiring() – włącza wywoływanie zdarzeń.
Te dwie metody są dostępne w instancji SPItemEventReceiver i nie ma do nich dostępu zewnątrz.
Pomyślałem, że skoro coś takiego jest w zdarzeniach, to też musi być dostępne dla elementu listy. W końcu MS czasami w jakiś sposób aktualizuje te elementy, nie zmieniając w nich żadnych informacji oraz nie wywołując żadnych naszych zdarzeń.
No to przypatrzyłem się temu co robi Update i SystemUpdate():
Mała różnica prawda? Jedynie pierwszy parametr, który oznacza czy to jest update systemowy czy też nie :)
To co przykuło moją uwagę to ostatni parametr o nazwie suppresAfterEvents. Już sama nazwa mi się bardzo spodobała, jednak w celu upewnienia się, odpaliłem Google.com i pierwszy link na jaki natrafiłem to był opis własnośći SPImportSettings.SupressAfterEvents. Tutaj wklejam opis który wytłumaczy co przykuło moją uwagę:
Disabling the firing and handling of After events can have a big impact on the performance of import operations. However, when performance is not a concern, you might want to disable handling After events to avoid potential problems resulting from event handlers not running their business logic.
Trafiłem w sedno! Więc zabrałem się do napisania prostej metody, PerformSystemUpdate():
private bool PerformSystemUpdate() { // ostatni parametr - suppres after events :D // this.UpdateInternal(false, false, Guid.Empty, false, false, false, false, false, false); try { Type type = typeof(SPListItem); MethodInfo method = type.GetMethod("UpdateInternal", BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(bool), typeof(bool), typeof(Guid), typeof(bool), typeof(bool), typeof(bool), typeof(bool), typeof(bool), typeof(bool) }, null ); object[] parameters = new object[] { // System Update true, false, Guid.Empty, false, false, false, false, false, // Suppres After Events true }; object obj = method.Invoke(SPContext.Current.ListItem, parameters); } catch { return false; } return true; }
I boom (bomb(track)) zaczęło działać! :)
Dzięki tej metodzie mogę spokojnie zatwierdzić element, dodać załączniki i wywołać ustawianie uprawnień.
Sweet and easy :)
Mam nadzieję, że Tip się komuś przyda :)
Dobre rozwiazanie, Gutek. Niestety, jesli go uzywasz w projekcie, lamiesz licencje Microsoftu. Porownaj z komentarzem Spence’a: http://blog.mastykarz.nl/programmatically-creating-variation-hierarchies-sharepoint-2007/#comment-10476. Szkoda…
@Waldek
Pytanie, czy ktos musi o tym wiedziec ;) Co do unsupported to na 100% tak jest, ale ze to lamie licencje?
Postaram sie to sprawdzic i dam znac :)
bardzo użyteczne informacje, wcześniej nad czymś takim łamali sobie głowę moi znajomi (chyba zdusili problem)
mnie natomiast bardziej ciekawi możliwość wykorzystania zwykłego formularza dla elementu listy tak, żeby można było w 1 kroku jakiegoś workflow (eventHandler sobie napiszę ;) ) ustalić jakie pola są widoczne a jakie są dla edycji dla przypisanego wykonawcy kroku (1 krok – autor widzi i edytuje 6 pól), w 2 kroku jego przełożony widzi 6 pól z 1 kroku i dodatkowe 2 pola (wcześniej te pola były “ukryte” przed autorem wniosku), itp.
Jest to możliwe w Sharepoint ? bo wiem (i mam nawet działający kod) do “globalnego” ukrywania/właczania edycji dla pól z elementu listy.
pozdro.
@Joguras
W Twoim wypadku widze dwie opcje. Jedna tworzona specjalnie pod Twoj workflow, druga bardziej “generalna”.
1) robisz wlasna strone edycji/dodawania itp ktora w zaleznosci od statusu workflow bedzie wyswietlac odpowiednie pola. Dzieki czemu mozesz wykonac wiecej operacji, i bardziej zmodyfikowac wyglad w zaleznosci od potrzeb.
2) tutaj wlasnie wykorzystujesz wlasna implmentacje ListITemInterator. Tak jak to ma rozwiazane SPListDisplaySettings czy jakos tak na codeplex (kiedys o tym juz pisalem). Piszesz takiego literatora oraz strone do ustawienia wartosci. I teraz mozesz sobie tak to zrobic ze bedziesz podawal pole statusu workflow, na podstawie ktorego bedziesz decydowal ktore pola maja byc wyswietlane, nastepnie mozesz takze dodac info na temat tego kto jakie pola powinien widziec i w jakim statusie :) Brzmi ciezko, jednak tak nie jest. jak masz mozliwosc to podejrzyj sobie w .NET Reflector rozwiazanie Bewise z tym SPListDisplaySettings i jak zroszumiesz jak to on zrobil (poprzez Web.Properties) to sam bedziesz wstanie napisac cos takiego bez wiekszego problemu.
Wrazie pytan wal smialo :)
Gutek
Na razie jest kłopot z używaniem ReadOnly – na formularzu edycji elementu także ukrywa pole (kolumnę) zamiast ustawić jej wartość jako tylko do odczytu :( – tego na razie nie mogę zwalczyć
@Joguras
Ja rozumiem pole ReadOnly jako pole ktorego nie da sie ustawic z poziomu interfejsu a jedynie z poziomu kodu. Wiec zrozumiale jest to iz pole jest nie widoczne na formularzach New/Edit.
Moze zrobiebie prostego wlasnego typu pola byloby rozwiazaniem dla Ciebie? wartosc ReadOnly by sie ustawialo przy definicji pola na liscie a nie przy definicja pola dla WSS/MOSS
Gutek
Comments are closed.