Custom Actions Unleashed
If you would like to have some paragraphs translated from Polish to English please leave a comment.
Post ten miał być krótkim tekstem opisującym sposób dodania grupy menu do Site Actions, w trakcie pisania przeszedł on wiele metamorfoz za każdym razem powodowanych logicznym rozumowaniem. Pisałem o czyś o czym moglibyście nie wiedzieć, a i w Internecie nie byłoby łatwo tego znaleźć. W ten sposób dodawałem kolejne rozdziały i punkty. I nim się obejrzałem powstał naprawdę długi (i mam nadzieję dokładny) artykuł o sposobie zarządzania własnym menu w SharePoint. Dodatkowo podczas pisania artykułu powstało masę przykładów, pokazujących i testujących pewne rozwiązania. Wraz z przykładami powstały kolejne problemy, kolejne opisy i tak wszystko się ciągnęło aż do dzisiaj.
Jest to mój pierwsze tak długi post (i chyba nie ostatni), ale na pewno prędko drugiego takiego nie napiszę.
Mam nadzieję, że informacje zawarte w nim przydadzą się wam w pracy z SharePoint a post nie będzie bezużyteczną binarną informacją zachowaną gdzieś na serwerach Google przez kilka miesięcy.
Ze względu na wielkość postu, jest on także dostępny w formacie PDF do pobrania na samym dole (w załącznikach).
Chciałbym podziękować Łukaszowi Olbromskiemu, bo gdyby nie jego pytanie 2/3 tygodnie temu, w życiu bym się nie zainteresował tak CustomActions, dzięki! I mam nadzieję, że i Tobie ten post się przyda :)
Spis Treści
· Definicje XMLi
· Wdrożenie Custom Action
· Tworzenie Menu Items za pomocą JavaScript
· Tworzenie Menu Items za pomocą kodu
· Tworzenie Menu Items za pomocą kontrolki ASP.NET
· Zarządzanie Menu Items za pomocą kodu JavaScript
· Zarządzanie Menu Items za pomocą kodu .NET
· Informacja dodatkowa
· Podsumowanie
· Zasoby
Definicje XMLi
Tak jak większość rzeczy konfiguracyjny w MOSS/WSS, Site Actions dodaje się poprzez odpowiedni XML. XML ten składa się z jednego lub więcej tagów CustomAction. Poniżej schemat tego Tagu oraz jego dokładny opis (opis na MSDN):
<CustomAction
ContentTypeId = "Text"
ControlAssembly = "Text"
ControlClass = "Text"
ControlSrc = "Text"
Description = "Text"
GroupId = "Text"
Id = "Text"
ImageUrl = "Text"
Location = "Text"
RegistrationId = "Text"
RegistrationType = "Text"
RequireSiteAdministrator = "TRUE"
Rights = "Text"
Sequence = "Integer"
ShowInLists = "TRUE"
ShowInReadOnlyContentTypes = "TRUE"
ShowInSealedContentTypes = "TRUE"
Title = "Text">
<UrlAction
Url = "Text">
</UrlAction>
</CustomAction>
Opis poszczególnych atrybutów (jedynym nie opcjonalnym atrybutem jest Title):
· ContentTypeId – atrybut przyjmujący ID Content Type tak by dane menu pojawiało się tylko i wyłącznie wtedy kiedy działamy na danym Conent Type. Niestety, ani mi ani Kit Kai’s nie udało się za pomocą tego elementu powiązać akcji z Conent Type. Jednak jest obejście, które koncentruje się na wykorzystaniu dwóch dodatkowych atrybutów – UAKTUALNIENIE Reflector wyraźnie pokazuje iż dany element nie istnieje w definicji CustomAction. Dodatkowo Element ContentTypeId nie istnieje w XML Schema (12 HIVE/TEMPLATES/XML/WSS.XSD) od CustomAction. Więc można naprawdę zapomnieć o tym atrybucie;
· ControlAssembly – atrybut przyjmujący Assembly Name, lub Fully Qualified Assembly Name. Różnica w wartości polega na sposobie deployment. Jeżeli kod wrzucamy do GAC, to podajemy Fully Qualified Assembly Name, jeżeli kod wrzucamy do BIN to podajemy AssemblyName. Microsoft mówi, że tak czy siak assembly powinno być zainstalowane w GAC, mnie osobiście udało się tego nie robić, więc osobiście mówię, że nie trzeba, jednak zalecam wykorzystanie GAC. Atrybut służy zdefiniowaniu assembly, które będzie odpowiedzialne na rysowanie/dodawanie menu – będzie o tym mowa później w postcie;
· ControlClass – atrybut używany wraz z ControlAssembly. Służy zdefiniowaniu klasy, która będzie rysowała nam menu. Trzeba podać pełną nazwę klasy wraz z Namespace;
· ControlSrc – atrybut określający Link do kontrolki ASCX, która będzie dostarczała kod dla akcji menu. Dokładnie nie wiem jak to ma w tym wypadku działać. Jeżeli wiecie jak, to dopiszcie w komentarzach, uaktualnię post – UAKTUALNIENIE z tego co zauważyłem w Reflector to jeżeli nie zostały podane wartości ControlAssembly i/lub ControlClass, MS wywołuje kontrolkę podaną w ControlSrc w celu stworzenia elementu (kod poniżej). Jednak mimo usilnych prób nie udało mi się spowodować by ten kod działał. Dokładny opis problemu znajduje się w rozdziale Tworzenie Menu Items za pomocą kontrolki ASP.NET;
if(string.IsNullOrEmpty(sControlAssembly) || string.IsNullOrEmpty(sControlClass))
{
if(string.IsNullOrEmpty(sControlSrc))
{
throw new ArgumentException(SPResource.GetString("RequiredFeatureAttribute", new object[] { "ControlSrc", xnElementDefinition.Name, featdefElement.Id.ToString() }));
}
ctl = SPUtility.CreateUserControlFromVirtualPath(tctlPage, ControlSrcServerRelativeUrl(sControlSrc));
}
· Description – atrybut opisujący daną akcję. Pojawia się on zaraz pod tytułem akcji i jest widoczny dla użytkownika końcowego;
· GroupId – atrybut określający grupę do jakiej akcję przypisujemy. Możemy albo skorzystać z istniejących grup opisanych tutaj na MSDN lub stworzyć własną grupę do której będziemy podpisać nasze menu. Dla podpięcia menu do Site Actions, w GroupId podajemy wartość SiteActions. GroupId wykorzystuje atrybut Location;
· Id – atrybut określający ID elementu, może to być Guid lub unikatowa nazwa. Po raz kolejny opisy standardowych ID można znaleźć pod tym linkiem (ostatnia kolumna);
· ImageUrl – atrybut określa URL do rysunku, który pojawia się po lewej stronie tytułu akcji. Rysunek może znajdować się na portalu, jak i być pobierany z Internetu;
· Location – atrybut określa położenie danej grupy. Czyli jeże skorzystamy z wbudowanych GroupId to wykorzystujemy Location podany na tej stronie. Jeżeli zaś korzystamy z naszej własnej grupy, to korzystamy z Location podanym w definicji grupy. Dla naszego przykładu (Site Actions), Location powninen być równy: Microsoft.SharePoint.StandardMenu;
· RegistrationId – atrybut określający ProgId. Może to być ID ContentType, ID Listy lub jakikolwiek inne ID podczas którego ma się wyświetlić dany element menu. Za pomocą jego możemy zrobić obejście do atrybutu ConentTypeId.Mianowicie jeżeli w RegistrationId podamy ContentTypeId i w RegistrationType podamy ContentType to przypiszemy naszą akcję do danego Content Type;
· RegistrationType – atrybut określa typ przypisania akcji. Może zawierać on tylko i wyłącznie cztery wartości:
o ContentType – określa przypisanie elementu do danego typu Content Type. W tym momencie RegistrationId musi zawierać wartość ID danego Content Type;
o FileType – określa przypisanie elementu do określonego typu pliku;
o List – określa przypisanie elementu do listy. Jeżeli zostanie podany RegistrationId, akcja zostania przypisana do konkretnego typu listy;
o ProgId – nie wiem do czego tutaj może być wykorzystany ProgId, jeżeli ktoś ma pomysł lub wie, proszę o komentarz, uaktualnię opis.
· RequireSiteAdministrator – atrybut przyjmujący wartości TRUE i FALSE (domyślna wartość). Określa on czy dana akcja ma być dostępna tylko dla administratorów. Nie może ona być wykorzystana na elementach listy – to znaczy, możecie podać tą wartość do tworzenia własnych akcji dla elementów listy (drop down menu), jednak nie będzie ona miała na te elementy wpływu. Uprawnienia do drop down menu są opisywane w JavaScript podczas wyświetlania listy;
· Rights – atrybut określa grupę uprawnień, które musi posiadać użytkownik by widzieć daną akcję. Atrybut może zawierać wiele wartości oddzielonych od siebie przecinkiem. Wartości, które może zawierać są opisane tutaj. UWAGA, użytkownik musi posiadać WSZYSTKIE uprawnienia wymienione w Rights jeżeli ma widzieć dany element. Pominięcie tego atrybutu, powoduje, wyświetlenie akcji każdemu użytkownikowi chyba, że atrybut RequireSiteAdministrator został podany i ustawiony na TRUE;
· Sequence – atrybut określa położenie danej akcji w menu; Nie podanie go, dopisuje akcje na koniec menu, zaś podanie niskiej wartości, może umieścić akcję przed już domyślnie istniejącymi. Przyjmuje wartość w postaci liczby. Na ten stronie możecie znaleźć informacje o standardowych ID Sequence dla akcji, jednakże jest mały Bug, który powoduje, że nie koniecznie Sequence wam zadziała. Krótką informacja na ten temat możecie znaleźć tutaj;
· ShowInLists – atrybut do określenia;
· ShowInReadOnlyContentTypes – atrybut do określenia;
· ShowInSealedContentTypes – atrybut do;
· Title – atrybut określenia tytuł akcji;
· UrlAction – tag określa akcję jaką dany element ma wykonać. Zawiera on tylko i wyłącznie jeden atrybut, zaś sam tag może wystąpić 0 lub 1 raz w danej CustomAction:
o URL – atrybut określa URL akcji jaka wykona się po kliknięciu na przycisk. Tutaj warto powiedzieć kilka słów o samych URL’ach. Taki URL może zawierać różne wartości, w tym także parametry obsługiwane przez SharePoint. Do takich parametrów zalicza się {ListId}, {ItemId} czy {SiteUrl}. Bardzo dobry artykuł opisujący te wszystkie parametry można zaleźć na MSDN: How to Add Actions to the User Interface. Podaje link a nie opisuje, dlatego, że warto zwrócić w nim uwagę na Community Content, który opisuje problemy jaki mogą się przytrafić podczas wykorzystywania tych parametrów. W tym problem wykorzystania 2 krotnie jednego parametru w jednym linku. Dodatkowo polecam ten link – opisuje on obejście problemu z podmianą podwójną parametru, oraz ten link – opisuje on obejście problemu z podmianą ListId. Choć osobiście sam się dziwię temu problemowi. Microsoft w swoim kodzie wykonuje funkcję Replace na string. Co powinno dawać wynik zamieniania wszystkich wystąpień danego elementu w ciągu znaków. Poniżej znajdziecie dwa kody które MS wykonuje podczas zamiany (wyciąłem tylko wszystkie ify). Jeden wykonywany jest po stronie .NET drugi po stronie JavaScript. Wszystko zależy od tego, z jakiego rodzaju CustomAction korzystacie. Warto przy tym zwrócić uwagę, że w implementacji JavaScript nie jest dostępny parametr RecurrenceId.
// .NET Code
private static string ReplaceUrlTokens(string urlAction, SPWeb web, SPList list, SPListItem item)
{
string newValue = item.ID.ToString(CultureInfo.InvariantCulture);
urlAction = urlAction.Replace("{ItemId}", newValue);
urlAction = urlAction.Replace("{ItemUrl}", item.Url);
urlAction = urlAction.Replace("{SiteUrl}", web.Url);
urlAction = urlAction.Replace("{ListId}", list.ID.ToString("B"));
string recurrenceID = item.RecurrenceID;
urlAction = urlAction.Replace("{RecurrenceId}", recurrenceID);
}
// JavaScript Code
function ReplaceUrlTokens(urlWithTokens, ctx)
{
urlWithTokens=urlWithTokens.replace("{ItemId}", currentItemID);
urlWithTokens=urlWithTokens.replace("{ItemUrl}", currentItemFileUrl);
urlWithTokens=urlWithTokens.replace("{SiteUrl}", ctx.HttpRoot);
urlWithTokens=urlWithTokens.replace("{ListId}", ctx.listName);
return urlWithTokens;
}


Dodatkowo, użytkownikowi dostarczona jest także możliwość chowania Custom Action. Chowanie, umożliwia zablokowanie wyświetlenia danej akcji. Np.: Mamy już jakieś menu, które dla naszych potrzeb, musi zostać ukryte przed użytkownikiem. W tym celu wykorzystujemy tag HideCustomAction. Jego schemat można znaleźć na MSDN lub poniżej:
<HideCustomAction
GroupId = "Text"
HideActionId = "Text"
Id = "Text"
Location = "Text">
</HideCustomAction>
Opis poszczególnych atrybutów (o dziwo, każdy atrybut jest opcjonalny :D):
· GroupId – atrybut określa grupę w której znajduje się akcja do ukrycia. Wartość ta jest tym samym czym wartość w CustomAction @GrupId, dlatego tutaj nie będę się nad tym rozpisywał. Warto zaznaczyć tylko, że GroupId może także wskazywać na naszą własną grupę. Przypominam tutaj link do MSDN;
· HideActionId – atrybut określa ID akcji, która powinna zostać ukryta. Wartości dla wbudowanych Action ID podobnie jak GroupId oraz Location można znaleźć pod tym adresem;
· Id – atrybut określa ID elementu HideCustomAcation i służy on jedynie naszemu łatwemu rozpoznaniu do czego ten HideAction służy. Wartością może być GUID jak i nazwa tekstowa;
· Location – atrybut określa położenie danej grupy w której jest element do ukrycia – przyjmuje te same wartości jak CustomAction @Location. Przypominam tutaj link do MSDN.
No i na sam koniec, mamy jeszcze element CustomActionGroup, który określa grupy dla CustomAction i HideCustomAction. Element ten ma jedynie znaczenie dla stron gdzie renderowane są grupy elementów np.: Site Settings czy List Settings. Jednakże nie działa on jako grupowanie elementów w Site Actions. Do tego należy niestety stworzyć kod o czym będzie później. Dodatkowo, element grupy ma dopiero znaczenie gdy zawiera CustomAction odwołujący się do niego. W przeciwnym wypadku element nie jest wyświetlany użytkownikowi.
Schemat CustomActionGroup można zaleźć poniżej lub na stronach MSDN:
<CustomActionGroup
Description = "Text"
Id = "Text"
Location = "Text"
Sequence = "Integer"
Title = "Text">
</CustomActionGroup>
Opis poszczególnych atrybutów (jedynie atrybuty Title i Location są wymagane):
· Description – atrybut określa opis grupy który jest wyświetlany jako pod-opis (atrybut Title jest traktowany jako opis) grupy;
· Id – atrybut określa ID grupy. Może to być GUID lub unikatowa nazwa. Ważne jest by następnie elementy CustomAction lub HideCustomAction zawierały ten ID w swoich definicjach;
· Location – atrybut określa miejsce w którym dana grupa istnieje. Ważne jest by wartość ta była wybrana już z istniejących opisanych w znanym już linku;
· Sequence – atrybut określa priorytet położenia grupy, zupełnie podobnie jak w CustomAction @Sequence z tym wyjątkiem, że to udało mi się stworzyć tak, że dział :);
· Title – atrybut określa nazwę grupy. Tytuł jest wyświetlany użytkownikowi np.: Users and Permissions w Site Settings.

No dobrze :) to tyle jeżeli chodzi o konstrukcje XMLowe. Przykłady wykorzystujące opisane TAGi zostały dołączone do postu, także jak i schematy elementów pobrane z WSS.XSD. Teraz pora poruszyć pozostałe kwestie: wdrażanie Custom Actions, tworzenie elementów menu przypisanych do item za pomocą JavaScript, tworzenie elementów menu za pomocą kodu, tworzenie elementów menu za pomocą kontrolek oraz zarządzanie elementami menu za pomocą kodu (np.: WebPart).
Wdrożenie Custom Action
Samo wdrożenie własnego menu, nie różni się niczym od wdrażania własnych Feature. W tym celu tworzymy własny feature.xml oraz elements.xml. W feature.xml określamy nazwę i tytuł naszego feature, zaś w elements.xml umieszczamy nasze CustomAction. Instalacja przebiega także bezboleśnie za pomocą stsadm:
stsadm -o installfeature -name MyFeatureName
Czyli nasz Feature.xml może wyglądać tak:
<Feature Id="1D4201C5-6005-4905-963A-89EC9C057909"
Title="MyFeatureName"
Description="This feature adds custom actions"
Version="1.0.0.0"
Scope="Site"
xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementManifest Location="elements.xml"/>
</ElementManifests>
</Feature>
Zaś elements.xml tak:
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction
Id="PeopleAndGroupsSiteAction"
GroupId="SiteActions"
Location="Microsoft.SharePoint.StandardMenu"
Title="People and Groups"
ImageUrl="/_layouts/images/Actionscreate.gif">
<UrlAction
Url="/_layouts/people.aspx" />
</CustomAction>
</Elements>
Jeżeli chcemy by feature był od razu aktywowany, to należy jeszcze wywołać komendę:
stsadm -o activatefeature -name MyFeatureName -url http://URL_TO_MY_SITE
Tworzenie Menu Items za pomocą JavaScript
W MOSS/WSS w katalogu 12 HIVE/TEMPLATES/LAYOUTS/1033 znajduje się plik CORE.JS a w nim aż trzy funkcje, które nas interesują:
1. AddListMenuItems – funkcja odpowiedzialna jest za stworzenie menu kontekstowego dla elementów listy (np.: Custom List, Task List i tym podobnym). Ważnym elementem tej funkcji jest sprawdzenie na samym początku czy funkcja Custom_AddListMenuItems istnieje i jeżeli istnieje to jej wywołanie. To właśnie dzięki własnej funkcji jesteśmy wstanie stworzyć własne elementy menu za pomocą JavaScript. Jeżeli zwróci się true to nie zostaną dodane żadne standardowe elementy, jeżeli zaś zostanie zwrócona wartość false to domyślne elementy menu zostaną dodane;
function AddListMenuItems(m, ctx)
{
if (typeof(Custom_AddListMenuItems) != "undefined")
{
if (Custom_AddListMenuItems(m, ctx))
return;
}
// Standardowy kod do tworzenia elementów
}
2. AddDocLibMenuItems – funkcja odpowiedzialna jest za stworzenie menu kontekstowego dla elementów Document Library. Tak jak poprzednio, funkcja ta na samym początku sprawdza czy funkcja Custom_AddDocLibMenuItems istnieje i jeżeli tak to ją wywołuje. To o czym należy pamiętać pisząc własną funkcję to zwracana wartość. Jeżeli zwróci się true to nie zostaną dodane żadne standardowe elementy, jeżeli zaś zostanie zwrócona wartość false to domyślne elementy menu zostaną dodane;
function AddDocLibMenuItems(m, ctx)
{
if (typeof(Custom_AddDocLibMenuItems) != "undefined")
{
if (Custom_AddDocLibMenuItems(m, ctx))
return;
}
// Standardowy kod do tworzenia elementów
}
3. InsertFeatureMenuItems – funkcja odpowiedzialna jest za dodanie elementów do menu kontekstowego wgranych za pomocą features. To ona decyduje jakie elementy i gdzie zostaną dodane. Warto się z tą funkcją zapoznać.
Skoro wiemy już jak to działa i co trzeba przeciążyć to napiszmy kod, który doda nam funkcję wyświetlającą alert do normalnych list z napisem: Udalo sie NAZWA_ELMENTU!, zaś do Document Libary alert dla plików Word: Udalo sie NAZWA_ELEMENTU!. Obie wiadomości będą się wyświetlały po tym jak klikniemy na przycisk menu Czy sie udalo?
Zanim przejedzmy do kodu, musimy jeszcze opisać jakiego typu parametry do niego przekazujemy:
· m – reprezentuje obiekt menu;
· ctx – udostępnia informację na temat Web Request w danym HTTP Context. Jeżeli jesteście ciekawi jakie wartości zawiera ctx, to zapraszam do pliku 12 HIVE/TEMPLATES/LAYOUTS/1033/INIT.JS – przeszukajcie go w poszukiwaniu „function ContextInfo()”.
Kod dla normalnych list wygląda następująco:
function Custom_AddListMenuItems(m, ctx)
{
// Tekst, ktory ma sie wyswietlic w menu kontekstowym
var strDisplayTextCustom = 'Czy sie udalo?';
// Pobranie nazwy elementu (Title)
var elementTitle = itemTable.innerText;
// Akcja ktora ma zostac wykonana po kliknieciu na element
var strAction = "alert('Udalo sie " + elementTitle + "!')";
/*
* Rysunek wybralem pierwszy lepszy... jezeli chcecie inne
* to mozecie sobie przeszukac katalog 12 HIVE/TEMPLATES/IMAGES
* jest tam tego sporo i napewno cos sie znajdzie, jak nie to
* zawsze moze dograc wlasny rysunek :)
*/
var strImagePath = ctx.imagesPath + "32316.GIF";
// Dodanie elementu do menu
CAMOpt(m, strDisplayTextCustom, strAction, strImagePath);
// Dodanie seperatora w menu
CAMSep(m);
return false;
}
Zanim przejdziemy dalej warto tu wytłumaczyć działanie dwóch funkcji:
· CAMOpt – funkcja odpowiedzialna za tworzenie elementu menu. Przyjmuje następujące parametry:
o menu – dla którego dany element ma zostać stworzony;
o display text – tekst który ma się wyświetlić na elemencie;
o action – akcja jaka ma się wykonać na kliknięcie na elemencie;
o image path – rysunek jaki ma się pokazać po przy tekście elementu.
· CAMSep – funkcja tworzy separator pomiędzy poszczególnymi elementami menu (linia separacyjna). Funkcja przyjmuje tylko jeden parametr menu dla którego należy stworzyć linię separacyjną.
Po zapisaniu pliku CORE.js (lub patrz UAKTUALNIENIE na końcu tego punktu) i otwarciu strony i listy w menu kontekstowym od naszego elementu pokaże się następująca opcja:



Zaś po kliknięciu na nią opali się nam następująca wiadomość:

Teraz bardzo podobny kod piszemy dla Document Library:
function Custom_AddDocLibMenuItems(m, ctx)
{
// Tekst, ktory ma sie wyswietlic w menu kontekstowym
var strDisplayTextCustom = 'Czy sie udalo?';
// Ustawienie typu dokumentu
setDocType();
/*
* Sprawdzenie typu aplikacji, w tym wypadku word, jednak
* wystarczy pomienic slowo word na excel i mamy juz kolejna
* applicaje.
*/
if(currentItemAppName.toLowerCase() == "microsoft office word")
{
// Pobranie nazwy elementu (Title)
var elementTitle = itemTable.innerText;
// Akcja ktora ma zostac wykonana po kliknieciu na element
var strAction = "alert('Udalo sie " + elementTitle + "!')";