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

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 GroupIdpodajemy 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 wbudowanychGroupId 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 wRegistrationId 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:
    • ContentType – określa przypisanie elementu do danego typu Content Type. W tym momencie RegistrationId musi zawierać wartość ID danego Content Type;
    • FileType – określa przypisanie elementu do określonego typu pliku;
    • List – określa przypisanie elementu do listy. Jeżeli zostanie podany RegistrationId, akcja zostania przypisana do konkretnego typu listy;
    • 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 atrybutRequireSiteAdministrator 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:
    • 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 CustomActionkorzystacie. 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;
}

gutek_ca_01

gutek_ca_02

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 tagHideCustomAction. 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 dlaCustomAction 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 (atrybutTitle 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 wCustomAction @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.

gutek_ca_03

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ę trueto 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ś doDocument 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:
    • menu – dla którego dany element ma zostać stworzony;
    • display text – tekst który ma się wyświetlić na elemencie;
    • action – akcja jaka ma się wykonać na kliknięcie na elemencie;
    • 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:

gutek_ca_04

gutek_ca_05

gutek_ca_06

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

gutek_ca_07

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 + "!')";
     
        /*
         * W tym wypadku rysunek podal mi MOSS/WSS, typ dokumnetu byl
         * Office 2003 XML i taki rysunek byl wyswietlany, wiec
         * taki i ja wyswietlilem.
         */
        var strImagePath = ctx.imagesPath + "ichtmdoc.gif";
     
        // Dodanie elementu do menu
        CAMOpt(m, strDisplayTextCustom, strAction, strImagePath);
 
        // Dodanie seperatora w menu
        CAMSep(m);
    }
       
    return false;
}

Po zapisaniu taki będzie rezultat:

gutek_ca_08

gutek_ca_09

Podczas tworzenia menu kontekstowego można jeszcze wspomnieć o funkcji HasRights, która sprawdza czy dana osoba ma odpowiednie uprawnienia i jeżeli posiada osoba takie uprawnienia to zostaje zwrócona wartość true w przeciwnym wypadku wartość false. Daje to możliwość tworzenia poszczególnych elementów menu tylko dla tych osób, które posiadają odpowiednie uprawnienia.

Funkcja przyjmuje dwa parametry, pierwszym jest maska permission dla najwyższych uprawnień, drugim maska permission dla najniższych uprawnień. Jednakże zabawa tym nie należy do najłatwiejszych. Ciężko jest się połapać co robi jaka maska. Dlatego jeżeli chcecie się tym bawić to polecam raczej popatrzenie w kod metod JavaScript i zobaczenie jaką maskę wykorzystuje MOSS/WSS by wyświetlić dany element menu. Dzięki czemu szybko będziecie wstanie znaleźć te maski, które was interesują.

Jeżeli jednak chcecie się zagłębić i zrozumieć maski to polecam następujące artykuły/posty:

Gdybyście trafili na jakieś dodatkowe ciekawe źródła to proszę podzielcie się :)

UAKTUALNIENIE: w tekście opisałem iż zapisujemy plik CORE.JS, nie jest to konieczne. Możemy tą funkcję wgrać za pomocą Content Editor WebPart, Master page, SharePoint Designer czy też własnej kontrolki ASCX. Każda z wyżej wymienionych metod dodania kodu JavaScript do strony została opisana w punkcie Zarządzanie Menu Items za pomocą kodu JavaScript. Oczywiście to nie są „wszystkie” metody za pomocą jakich można wgrywać JavaScript. Jednak są to dość szybkie metody, inne przeważnie są stosowane przy bardziej zaawansowanych rozwiązaniach – na przykład site templates itp. itd. Opisałem te gdyż uważam, że warto o nich wiedzieć, a reszcie można dowidzieć się z czasem :)

Tworzenie Menu Items za pomocą kodu

Teraz w końcu coś dla programistów :) Zanim przejdę do dokładnych opisów, stwórzmy sobie projekt ASP.NET Web Application, dodajmy do niego referencję do Microsoft.SharePoint.dll, skasujmy Default.aspx, dodajmy podpisywanie kluczem.

Możemy zaczynać :)

Naszym celem jest stworzenie Grupy w Site Actions o nazwie Wyszukiwarki a w niej trzy linki do wyszukiwarek: Google, Live i WSS/MOSS Search Site.

Stwórzmy sobie więc nowy klasę i nazwijmy ją SearchSiteMenu. Klasa ta powinna dziedziczyć po WebControl i jedyną metodą nas interesującą w całej klasie jest przeciążona metoda CreateChildControls. Szablon klasy powinien wyglądać tak:

/// <summary>
/// Tworzy grupe menu z linkami do popularnych wyszukiwarek.
/// </summary>
public class SearchSiteMenu : WebControl
{
    /// <summary>
    /// Generuje elementy menu.
    /// </summary>
    protected override void CreateChildControls()
    {
    }
}

Teraz, musimy stworzyć naszą grupę. Grupy w takich menu jak Site Actions czy Actions menu, tworzymy za pomocą klasy SubMenuTemplate:

// Tworzymy grupe menu.
SubMenuTemplate searches = new SubMenuTemplate();
searches.Text = "Wyszukiwarki";
searches.Description = "Linki do popularnych wyszukiwarek internetowych";
// Przyklad pobrania rysunku z Internetu
searches.ImageUrl = "http://www.iconico.com/magnifier/MagnifierIcon.jpg";

To co nas interesuje z własności klasy to:

  • Text – określa tekst wyświetlany w menu (tak jak atrybut CustomAction @Title);
  • Description – określa opis elementu wyświetlany pod tytułem (tak jak atrybutCustomAction @Description);
  • ImageUrl – określa URL do obrazka, który ma się pokazać przy tytule (tak jak atrybutCustomAction @ImageUrl);
  • Sequence – osobiście go nie używam, ale jego zadaniem jest umiejscowienie elementu menu w odpowiednim miejscu (tak jak atrybut CustomAction @Sequence).

Teraz by tą grupę dodać należy wywołać metodę:

// Dodajemy grupe do kontrolek
this.Controls.Add(searches);

Jednak to tworzy nam jedynie pustą grupę. Jeżeli chcemy dodać do niej element, musimy stworzyćMenuItemTemplate i dodać go do kolekcji kontrolek SubMenuTemplate. Ze względu na to, iż element musi stworzyć aż trzy razy, wyciągamy część wspólną do funkcji:

/// <summary>
/// Tworzy pojedynczy element menu.
/// </summary>
/// <param name="title">Tytul elementu jaki ma sie wyswietlic.</param>
/// <param name="description">Opis elementu jaki ma sie wyswietlic pod tytulem.</param>
/// <param name="imageUrl">URL do obrazka (jezeli jakis ma byc).</param>
/// <param name="url">URL na ktory element ma kierowac.</param>
/// <returns>
/// Pojedynczy element menu.
/// </returns>
private MenuItemTemplate CreateMenuItem(string title, string description, string imageUrl, string url)
{
    MenuItemTemplate menuItem = new MenuItemTemplate();
    menuItem.Text = title;
    menuItem.Description = description;
    menuItem.ImageUrl = imageUrl;
    menuItem.ClientOnClickNavigateUrl = url;
 
    return menuItem;
}

Tak jak poprzednio mamy kilka własności, które należy uzupełnić (oraz kilka, które można uzupełnić), pełna lista moim zdaniem ważnych własności znajduje się poniżej:

  • Text – określa tekst wyświetlany w menu (tak jak atrybut CustomAction @Title);
  • Description – określa opis elementu wyświetlany pod tytułem (tak jak atrybutCustomAction @Description);
  • ImageUrl – określa URL do obrazka, który ma się pokazać przy tytule (tak jak atrybutCustomAction @ImageUrl);
  • ClientOnCLickNavigateUrl – określa URL do którego ma zostać przekierowany użytkownik po kliknięciu na przycisk (tak jak atrybut CustomAction UrlAction @Url);
  • ClientOnClickScript – określa skrypt jaki ma zostać wywołany przed wykonaniem akcji. Na przykład można poprosić użytkownika o potwierdzenie usunięcia elementu. Własność ta jest ustawiana na końcu metody set własności ClientOnClickUsingPostBackEvent, dlatego też jeżeli korzystamy z PostBackEvent to nie możemy wykorzystaćClientOnClickScript bo stracimy informacje dot. zdarzenia postback;
  • ClientOnClickUsingPostBackEvent – określa parametry zdarzenia postback. Własność korzysta z własności ClientOnClickPostBackConfirmation, więc ważna jest kolejność ich deklaracji. Jeżeli określimy ClientOnClickPostBackConfirmation po zdefiniowaniuClientOnClickUsingPostBackEvent to nasza własność PostBack nie wywoła zapytania PostBackConfirmation. Dodatkowo jeżeli określimy ClientOnClickScript po zdefiniowaniuClientOnClickUsingPostBackEvent to nadpiszemy nasz skrypt post back;
  • ClientOnClickPostBackConfirmation – określa pytanie, jakie powinno zostać zadane przed wykonanie zdarzenia określonego przez ClientOnClickUsingPostBackEvent. Nie podanie własności ClientOnClickUsingPostBackEvent spowoduje iż określenieClientOnClickPostBackConfirmation nie będzie miało na nic wpływu;
  • Sequence – osobiście go nie używam, ale jego zadaniem jest umiejscowienie elementu menu w odpowiednim miejscu (tak jak atrybut CustomAction @Sequence);
  • Permissions – określa zbiór uprawnień jakie są potrzebne by dany element się wyświetlił użytkownikowi. Różnica pomiędzy własnościami Permissions a PermissionsString jest taka, że Permissions jest wynikiem połączenia poszczególnych wartości SPBasePermissions. Różnica pomiędzy implementacją tego w CustomActions a w MenuItemTemplate jest taka iż użytkownikowi dostępna jest jeszcze jedna własnośćPermissionMode;
  • PermissionMode – określa czy użytkownik musi posiadać wszystkie uprawnienia wypisane w PermissionsString lub Permissions, czy wystarczy że będzie posiadał jedno z wymienionych. PermissionMode przyjmuje dwie wartości: All i Any;
  • PermissionsString – określa zbiór uprawnień jakie są potrzebne by dany element się wyświetlił użytkownikowi (tak jak atrybut CustomAction @Rights).

Skoro mamy już prawie wszystko gotowe, dodajmy brakujący kod. Pomiędzy deklaracją SubMenuTemplate i dodaniem menu do kontrolek, dorzucamy następujący kod:

// Dodajemy do grupy link do Google
searches.Controls.Add(this.CreateMenuItem(
        "Google",
        "Najlepsza wyszukiwarka internetowa",
        "http://www.evancarmichael.com/SEO-For-Africa/Google-logo-small.jpg",
        "http://www.google.com")
);
 
// Dodajemy do grupy link do Live
searches.Controls.Add(this.CreateMenuItem(
        "Live",
        "Odpowiedz Microsoft na wyszukiwarke Google",
        "http://upload.wikimedia.org/wikipedia/en/thumb/2/2f/Windows_Live_Search_logo.png/20px-Windows_Live_Search_logo.png",
        "http://www.live.com")
);
 
// Dodajemy do grupy link do WSS 3.0 Search
searches.Controls.Add(this.CreateMenuItem(
        "WSS 3.0 Search",
        "Wyszukiwarka na stronie WSS 3.0",
        "/_layouts/images/titlegraphic.gif",
        "/_layouts/searchresults.aspx")
);

Teraz, nasz projekt buildujemy, DLL dodajemy do GAC, do WebConfig od MOSS/WSS dodajemy wpis SafeControls (bez tego nie zadziała):

<SafeControl Assembly="Gutek.SharePoint.WebControls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=97f67eae42644119" Namespace="Gutek.SharePoint.WebControls" TypeName="*" Safe="True" AllowRemoteDesigner="True" />

I tworzymy nasze pliki XML:

<!-- Feature XML -->
<Feature
  Id="EEDF9048-715D-11DD-9CE6-2DAE56D89593"
  Title="Gutek Site Action Menus"
  Description="Przykladowe site actions menu"
  Scope="Site"
  xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests>
    <ElementManifest Location="elements.xml" />
  </ElementManifests>
</Feature>
 
<!-- Elements XML -->
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction
        Id="{5F77B506-BDC6-4c94-B22E-46B9E6C469DA}"
        Location="Microsoft.SharePoint.StandardMenu"
        GroupId="SiteActions"
        ControlAssembly="Gutek.SharePoint.WebControls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=97f67eae42644119"
        ControlClass="Gutek.SharePoint.WebControls.SearchSiteMenu">
  </CustomAction>
</Elements>

Gotowe XML wrzucamy do katalogu MojeSiteActionMenu stworzonego w 12 HIVE/TEMPLATES/FEATURES i z 12 HIVE/BIN wywołujemy stsadm:

stsadm -o installfeature -name MojeSiteActionMenu

stsadm -o activatefeature -name MojeSiteActionMenu -url http://pearljam.com

iisreset

Po operacjach stsadm jeżeli wejdziemy sobie na stronę MOSS/WSS to w Site Actions będziemy mieli nasze własne menu:

gutek_ca_10

Jak pewnie zauważyliście uprawnienia do menu, można w ten sposób ustawiać podwójnie, raz za pomocą CustomActions a raz z pomocą kodu źródłowego w MenuItemTemplate. Daje nam to możliwość dokładniejszego wyspecyfikowania uprawnień w zależności od zapotrzebowania. Także warto tutaj zwrócić uwagę na rozdział Informacja Dodatkowa, w którym opisuje sposób przechowywania ustawień które można wykorzystać w naszym kodzie.

Dodatkowym plusem napisania własnego menu, jest możliwość określenia w kodzie kiedy menu ma się wyświetlić i tak dla przykładu możemy wykonać następujący kod:

/// <summary>
/// Generuje elementu menu.
/// </summary>
protected override void CreateChildControls()
{
    // Tworzymy grupe menu.
    SubMenuTemplate list = new SubMenuTemplate();
    list.Text = "Lista";
    list.Description = "Linki dla elementu listy w zaleznosci od kontekstu.";
    list.ImageUrl = "/_layouts/images/List.gif";
 
    switch(SPContext.Current.FormContext.FormMode)
    {
        case Microsoft.SharePoint.WebControls.SPControlMode.Display:
            list.Controls.Add(this.CreateListMenuItem());
            list.Controls.Add(this.CreateNewMenuItem());
            list.Controls.Add(this.CreateEditMenuItem());
            break;
        case Microsoft.SharePoint.WebControls.SPControlMode.Edit:
            list.Controls.Add(this.CreateListMenuItem());
            list.Controls.Add(this.CreateNewMenuItem());
            list.Controls.Add(this.CreateDisplayMenuItem());
            break;
        case Microsoft.SharePoint.WebControls.SPControlMode.New:
            list.Controls.Add(this.CreateListMenuItem());
            break;
        case Microsoft.SharePoint.WebControls.SPControlMode.Invalid:
        default:
            // Mozemy byc gdziekolwiek na stronie lub na widoku listy
            if(SPContext.Current.List != null)
            {
                // Jestesmy na widoku listy :)
                list.Controls.Add(this.CreateListMenuItem());
                list.Controls.Add(this.CreateNewMenuItem());
            }
            break;
    }
 
    if(list.Controls.Count > 0)
    {
        this.Controls.Add(list);
    }
}

Który w zależności od tego czy jesteśmy na widoku listy czy na widoku elementu doda nam do Site Actions dodatkowe elementy menu. Oczywiście to samo możemy zrobić za pomocą atrybutówCustomActions @RegistrationType i @RegistrationId, jednak tutaj mamy kontrolę dodatkową nad tym czy jesteśmy na Display Form od elementu czy na Edit Form od elementu.

Po zakończeniu (reszta klasy w przykładach załączonych do postu) i wgraniu naszego kodu, tak oto będzie on wyglądał w Site Actions:

gutek_ca_11

gutek_ca_12

Skoro potrafimy już tworzyć własne menu z kodu, to warto teraz poruszyć temat wywoływania PostBack event na kliknięcie elementu. Jak wiemy, przeważnie podajemy ClientOnCLickNavigateUrl i przechodzimy na jakąś stronę. Oczywiście ta strona może się nazwać DeleteItem.aspx i przyjmować parametry typu ListId i ItemId. Ale także możemy to zrobić krócej.

W tym celu nasza kontrolka musi dodatkowo implementować interfejs IPostBackEventHandler. Jeżeli chcemy stworzyć usunięcie elementu w naszej kontrolce to implementacja interfejsu powinna wyglądać następująco:

/// <summary>
/// Wywołuje zdarzenie Post Back.
/// </summary>
/// <param name="eventArgument">Argumenty zdarzenia.</param>
public void RaisePostBackEvent(string eventArgument)
{
    SPContext.Current.ListItem.Delete();
 
    SPUtility.Redirect(SPContext.Current.List.DefaultViewUrl, SPRedirectFlags.Default, this.Context);
}

Nasza metoda jedynie usuwa element i przekierowuje na stronę domyślnego widoku listy.

Teraz by wywołać to z naszej kontrolki tworzącej menu, musimy odpowiednio stworzyć element menu:

private MenuItemTemplate CreateDeleteMenuItem()
{
    string title = string.Format("Usun {0}", SPContext.Current.ListItem.Title);
    string description = string.Format("Usuwa element {0}", SPContext.Current.ListItem.Title);
 
    MenuItemTemplate menuItem = new MenuItemTemplate();
    menuItem.Text = title;
    menuItem.Description = description;
    menuItem.ClientOnClickScript = "if (confirm('Czy napewno chcesz usunac element?')) __doPostBack('" + this.UniqueID + "')";
 
    return menuItem;
}

Kluczem do sukcesu jest tutaj własność ClientOnClickScript odpowiednio z formułowany:

"__doPostBack('" + this.UniqueID + "')"

Powoduje on wywołanie zdarzenia post back dla danego elementu.

W opisie własności MenuItemTemplate wspomniałem o zależnościach pomiędzy trzema własnościami. Teraz, żeby było to łatwiej zrozumieć wytłumaczę to w następujący sposób:

gutek_ca_13

Jeżeli chcemy wykorzystać ClientOnClickUsingPostBackEvent w naszej kontrolce, to musimy wykonać dodatkowy krok, stworzyć własny bezparametrowy konstruktor w którym musimy ustawić własność ID naszej kontrolki:

public ListSiteMenu()
{
    this.ID = "superMojTest";
}

W kodzie zamiast przypisania ClientOnClickScript, możemy ustawić następujący kod:

menuItem.ClientOnClickPostBackConfirmation = "Czy napewno chcesz usunac element?";
menuItem.ClientOnClickUsingPostBackEvent = this.ID;

Ten kod zadziała :) i pomyśleć, że tylko tyle było wymagane by to zadziałało ;)

Warto wspomnieć iż w dwóch przypadkach, możemy podać parametry wykonania post back event:

"__doPostBack('" + this.UniqueID + "','%ID%')"

this.ID + ",%ID%"

Jednak wartości te muszą być zdefiniowane na stronie by były one podmienione. Dokładny opis podmieniania elementów tupu %ID% można znaleźć w trochę odmiennym postcie Pawlo:SPGridView and SPMenuField: Displaying custom data through SharePoint lists, który opisuje w jaki sposób te parametry mogą zostać zastąpione (prawie na końcu artykułu).

Dodatkowo polecam te dwa artykuły:

Oba poruszają tematykę post back na MenuItemTemplate i pokazują sposoby rozwiązania pewnych kwestii (jak na przykład parametry) czy własne MenuItemTemplate, który udostępnia zdarzenie PostBack, pod które można się podłączyć wykorzystując kontrolkę.

I na tym zakończę opisywanie jak za pomocą kodu stworzyć własne elementy menu. Oczywiście możliwości jest więcej, nie tylko można tworzyć własne elementy menu w Site Actions czy w Newmenu, ale także jak w ostatnim wpisie na blogu Pawlo, można tworzyć własne przycisku do Toolbar: Adding custom SharePoint Toolbar and ToolbarButtons in code jednak to wykracza poza zasięg treści tego postu.

Tworzenie Menu Items za pomocą kontrolki ASP.NET

Chciałbym powiedzieć, że tutaj sprawa jest prosta i przyjemna. Niestety tak nie jest :( Tak jak opisałem w definicji XML, by zadziałała kontrolka powinno się jedynie podać atrybutCustomAction @ControlSrc i jest to prawda. Jednakże kontrolka nie może zawierać kodu code-behind. Jaka kol wiek dyrektywa informująca o tym iż kontrolka posiada kod i dziedziczy po jakiejś klasie, czyli np.:

<%@ Control Language="C#" AutoEventWireup="true" Inherits="Gutek.SharePoint.WebControls.CustomSiteActionMenuItem, Gutek.SharePoint.WebControls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=97f67eae42644119" %>

Powoduje wyświetlenie się następującego błędu w 12 HIVE/LOGS:

Failed to instantiate custom control due to incorrectly specified Control attributes in Feature ‘MyMenus’ (ID: eedf9048-715d-11dd-9ce6-2dae56d89593) control ‘{AA4FE5AD-8F5E-442a-B995-94630AB59D07}’. ControlSrc ‘~/_controltemplates/CustomSiteActionMenuItem.ascx’, ControlAssembly ”, ControlClass.

Stwierdziłem więc iż jest to błąd deklaracji kontrolki w CustomActions. Spędziłem pół dnia doszukująć się błędu i próbując różnych kombinacji wciąż bez skutku – z takim samym rezultatem (jedynie pola w ciapkach pojedynczych zostały wypełnione odpowiednimi informacjami). Więc zacząłem poszukiwania opisu błędu w sieci (bez rezultatu) to w końcu w kodzie SharePoint. Natrafiłem na zmienną resource, która jest wykorzystywana podczas decyzji za pomocą czego ma być tworzone menu (kontrolka czy kod). Zmienna nazywa się: FailedToBuildCustomControl a odpowiedni ciąg znaków przypisanych do niej to:

Failed to instantiate custom control due to incorrectly specified Control attributes in Feature ‘|0’ (ID: |1) control ‘|2’. ControlSrc ‘|3’, ControlAssembly ‘|4’, ControlClass ‘|5’.

To dało mi możliwość przeszukania kodu SharePoint w poszukiwaniu wykorzystania tego klucza. Jest on wykorzystywany tylko i wyłącznie w jednym miejscu. I to dosłownie w tym w którym kod wam wkleiłem w opisie atrybutu. Poniżej ten sam kod tylko bez wnętrzności:

internal static Control BuildCustomControl(TemplateControl tctlPage, string sControlAssembly, string sControlClass, string sControlSrc, XmlNode xnElementDefinition, SPFeatureDefinition featdefElement, string sElementId)
{
    Control ctl = null;
    if(ValidControlAttributes(sControlAssembly, sControlClass, sControlSrc))
    {
        // Tutaj wycialem kod wklejony podczas opisu atrybuty
    }
    else
    {
        string str = SPResource.GetString("FailedToBuildCustomControl", new object[] { featdefElement.DisplayName, featdefElement.Id, sElementId, sControlSrc, sControlAssembly, sControlClass });
        ULS.SendTraceTag(ULSTagID.tag_8l17, ULSCat.msoulscat_WSS_Runtime, ULSTraceLevel.High, "%s", new object[] { str });
        return ctl;
    }
    SPUtility.SetPropertiesOnControlFromXml(ctl, xnElementDefinition);
    return ctl;
}

Więc jedynym powodem dla którego kontrolka może wyrzucać tak błąd jest jej nie poprawna walidacja. Niestety podczas analizy tej funkcji i funkcji która ona wywołuje wymiękłem. Niestety .NET Reflector nie radził sobie z ich dekompilacją i każda kolejna funkcja to była analiza IL. I miałem tego dość. Jednak doszedłem do wniosku iż jedynym błędem jaki mogę mieć to problem z wykryciem mojej kontrolki jako kontrolki SafeControls. Więc upewniłem się czy ona leży w dobrym miejscu a następnie nadałem jej taką sama deklarację jak inne kontrolki w SafeControls –w kwestii wyjaśnienia, w SafeControls znajduje się SafeControl definiujący położenie kontrolek ASCX w 12 HIVE/TEMPLATES/CONTROLTEMPLATES. I boom, zadziałało… to znaczy nie wyrzuciło już błędu w 12 HIVE/LOGS jednak wciąż nie widziałem tego co zrobiłem. I znów kilka prób różne sposoby dodania/edycji stworzenie pustego linku, dodanie kontrolki MenuItemTemplate za pomocą Tagu. Wciąż bez rezultatu.

Wyjściem okazało się potrójne obejście :) Po pierwsze zrezygnowanie z wykorzystania Tagu CustomAction, nawet już nie próbowałem z niego korzystać trochę się wkurzyłem po tych wszystkich analizach, zamiast niego wykorzystałem tag Control. Nie opisuje go tutaj, ale to nie jedyny raz w tym tekście gdzie ten tag występuje. Po drugie wykorzystałem tak zwany Delegate Control (tutaj opis jego wykorzystania: How to: Customize a Delegate Control). No i ostatecznie zmodyfikowałem plik master od strony.

Wadą obejścia jest to iż trzeba stworzyć w ten sposób pełne menu. To znaczy, jeżeli chcemy zastąpić Site Actions to zastępujemy je w całości. Oczywiście możemy stworzyć te same elementy menu co są w Site Actions ale na chłopski rozum, po co robić to co już jest? :(

A więc tak, jeżeli wciąż jesteście ciekawi jak to zrobić to tworzymy naszą kontrolkę ascx i do niej wrzucamy następujący kod:

<%@ Control Language="C#" %>
<%@ Assembly Name="Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls"
    Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
 
<SharePoint:SiteActions runat="server" AccessKey="<%$Resources:wss,tb_SiteActions_AK%>"
    ID="SiteActionsMenuMain" PrefixHtml="&lt;div &gt;"
    SuffixHtml="&lt;/div&gt;" MenuNotVisibleHtml="&amp;nbsp;">
    <CustomTemplate>
        <SharePoint:FeatureMenuTemplate ID="FeatureMenuTemplate1"
            runat="server"
            FeatureScope="Site"
            Location="Microsoft.SharePoint.StandardMenu"
            GroupId="SiteActions"
            UseShortId="true">
            <!-- Domyslne menu -->
            <SharePoint:MenuItemTemplate
                runat="server"
                ID="MenuItem_Create"
                Text="<%$Resources:wss,viewlsts_pagetitle_create%>"
                MenuGroupId="100"
                Sequence="100"
                UseShortId="true"
                ClientOnClickNavigateUrl="~site/_layouts/create.aspx"
                PermissionsString="ManageLists, ManageSubwebs"
                PermissionMode="Any" />
            <SharePoint:MenuItemTemplate
                runat="server"
                ID="MenuItem_EditPage"
                Text="<%$Resources:wss,siteactions_editpage%>"
                MenuGroupId="100"
                Sequence="200"
                ClientOnClickNavigateUrl="BLOCKED SCRIPTMSOLayout_ChangeLayoutMode(false);" />
            <SharePoint:MenuItemTemplate
                runat="server"
                ID="MenuItem_Settings"
                Text="<%$Resources:wss,settings_pagetitle%>"
                MenuGroupId="100"
                Sequence="300"
                UseShortId="true"
                ClientOnClickNavigateUrl="~site/_layouts/settings.aspx"
                PermissionsString="EnumeratePermissions, ManageWeb ,ManageSubwebs,
                        AddAndCustomizePages, ApplyThemeAndBorder,
                        ManageAlerts,ManageLists,ViewUsageData"
                PermissionMode="Any" />
            <!-- Nasze nowe menu -->
            <SharePoint:SubMenuTemplate
                ID="SubMenu_Searches" runat="server"
                MenuGroupId="100" Sequence="200"
                Text="Wyszukiwarki"
                Description="Linki do popularnych wyszukiwarek internetowych">
                <SharePoint:MenuItemTemplate
                    runat="server"
                    ID="MenuItem_Google"
                    Text="Google"
                    Description="Najlepsza wyszukiwarka internetowa"
                    MenuGroupId="100"
                    Sequence="200"
                    ClientOnClickNavigateUrl="http://www.google.com"/>
                <SharePoint:MenuItemTemplate
                    runat="server"
                    ID="MenuItem_Microsoft"
                    Text="Live"
                    Description="Odpowiedz Microsoft na wyszukiwarke Google"
                    MenuGroupId="100"
                    Sequence="200"
                    ClientOnClickNavigateUrl="http://www.live.com" />
                <SharePoint:MenuItemTemplate
                    runat="server"
                    ID="MenuItem_WSS"
                    Text="WSS 3.0 Search"
                    Description="Wyszukiwarka na stronie WSS 3.0"
                    MenuGroupId="100"
                    Sequence="200"
                    ClientOnClickNavigateUrl="/_layouts/searchresults.aspx" />
            </SharePoint:SubMenuTemplate>
        </SharePoint:FeatureMenuTemplate>
    </CustomTemplate>
</SharePoint:SiteActions>

Następnie nasz feature i element xml powinien wyglądać następująco:

<!-- Feature XML -->
<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
  Id="{77358BDB-35E9-4201-A1D8-1C1476110A03}"
  Title="Zmodyfikowane Site Actions"
  Description="Site Actions zmodyfkiowane za pomoca kontrolki"
  Version="1.0.0.0"
  Scope="Site"
  Hidden="FALSE">
  <ElementManifests>
    <ElementManifest Location="elements.xml" />
  </ElementManifests>
</Feature>
 
<!-- Elements XML -->
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Control
     Id="CustomSiteActionsFromControl"
     Sequence="50"
     ControlSrc="~/_controltemplates/CustomSiteActionMenuItem.ascx">
  </Control>
</Elements>

I by tego było mało to musimy wykonać operację podmiany kontrolki w master – najlepiej za pomocą SharePoint Designer (nie chcę tworzyć master specjalnie dla tego przykładu):

<SharePoint:SiteActions></SharePoint:SiteActions>

Na:

<SharePoint:DelegateControl
    ID="delegateSiteActionsControl"
    runat="server"
    ControlId="CustomSiteActionsFromControl" />

Ważne przy tym jest to, by ControlId był taki sam jak ID ustawiony w tagu Control.

Kod wgrywamy normalnie za pomocą stsadm, zaś kontrolkę możemy sami przekopiować do 12 HIVE/TEMPLATES/CONTROLTEMPLATES.

Jeżeli uda wam się zdziałać coś z atrybutem CustomAction @ControlSrc to proszę dajcie znać w komentarzach. Z chęcią poznam sposób stworzenia własnego menu inny niż tutaj opisałem.

Zarządzanie Menu Items za pomocą kodu JavaScript

Microsoft chcąc nam pomóc udostępnił nam tag HideCustomAction, niestety, nie działa on dla wszystkich możliwych elementów menu. Jednym z głównych ograniczeń (moim zdaniem) to lista elementów udostępniona przez Microsoft na stronie Default Custom Action Locations and IDs, wiemy zaś że tych elementów jest znacznie więcej SharePoint Custom Action Identifiers.

Gdyby komuś z was chciało się testować HideCustomActions ze wszystkimi możliwymi typami, to z chęcią zapoznam się z wynikami. Jednakże do tego czasu warto zastanowić się nad wykorzystaniem JavaScript do tego by zarządzać niektórymi elementami.

Teraz, dla przykładu, jeżeli chcemy spowodować by element Edit In Datasheet nie pokazywał się menu Actions możemy napisać następujący kod:

<script language="JavaScript" type="text/javascript">
    // Kazdy element menu to tag ie:menuitem
    var ele = document.getElementsByTagName('ie:menuitem');
   
    for (var i = 0; i < ele.length; i++)
    {
        var myMenuItem = ele(i);
       
        if (myMenuItem.id.match('EditInGridButton') != null)
        {
            myMenuItem.hidden = true;
        }
    }
</script>

Za pomocą tak skonstruowanego kodu, jesteśmy wstanie ukryć większość elementów menu z tool bar. Jednakże jedynym problemem w tym rozwiązaniu jest jego instalacja. Żeby kod zadziałał, należy go dodać do strony w jeden z czterech możliwych sposobów:

  1. SharePoint Designer – kod możemy dodać tuż przed Tagiem zamykającym <asp:Content ContentPlaceHolderId="PlaceHolderMain" runat="server"> dla każdej strony widoku listy (np.: AllItems.aspx). Daje nam to możliwość kontrolowania gdzie i na jakim widoku odpowiednie elementy menu powinny zostać ukryte/pokazane itp. Dodatkowo można dorobić kod sprawdzający uprawnienia (HasRights), dzięki czemu mamy możliwość zmiany pewnych zachowań menu z JavaScript, które zostały narzucone za pomocą CustomActionslub CORE.JS. Dodatkowo w SPD możemy także zaktualizować stronę master n której doda się kod JavaScript a który umożliwi nam odpowiednie zarządzanie elementami. SPD daje nam możliwość dokładnej kontroli gdzie i kiedy nasz skrypt ma działać, globalnie (na wszystkie listy) czy lokalnie (na określony widok);
  2. Master – poprzez modyfikację pliku master i jego wgranie, możemy dowolnie umieszczać własne skrypty JavaScript, które będą wykonywać operację przez nas oprogramowane. Master działa globalnie, modyfikacja jego będzie miała wpływ na menu we wszystkich listach;
  3. Content Editor Web Part – poprzez modyfikację parametru Source WebPartu możemy podać w nim kod JavaScript, który będzie się uruchamiał podczas ładowania WebPartu. Plusem tego rozwiązania jest możliwość eksportu naszego Content Editor Web Part jako WebPart template i możliwość jego wykorzystania na innych stronach bez konieczności jego modyfikowania. Niestety umożliwia on jedynie kontrolę nad elementami menu stronynie zaś nad elementami menu List View Toolbar. Poniższy kod schowa nam z Site Actionselement Edit Page:

    <script language="JavaScript" type="text/javascript">
        // Kazdy element menu to tag ie:menuitem
        var ele = document.getElementsByTagName('ie:menuitem');
       
        for (var i = 0; i < ele.length; i++)
        {
            var myMenuItem = ele(i);
           
            if (myMenuItem.id.match('EditPage') != null)
            {
                myMenuItem.hidden = true;
            }
        }
    </script>

    gutek_ca_14

    gutek_ca_15

  4. Feature – możemy także stworzyć własny Feature, który wgra kontrolkę na serwer i będzie miał on kontrolę nad elementami menu w określonym Scope. Poniżej przykład takiego Feature, który by wykonywał nasz kod JS. Najpierw tworzymy kontrolkę ASCX zawierającą nasz kod JavaScript, następnie tworzymy Feature.xml i na końcu Elements.xml z Tagiem Control (informacje na temat Control na MSDN):

    <!-- Kontrolka ASCX -->
    <%@ Control Language="C#" ClassName="JavaScriptHideMenuItems" %>
    <script language="JavaScript" type="text/javascript">
        // Kazdy element menu to tag ie:menuitem
        var ele = document.getElementsByTagName('ie:menuitem');
       
        for (var i = 0; i < ele.length; i++)
        {
            var myMenuItem = ele(i);
           
            if (myMenuItem.id.match('EditInGridButton') != null)
            {
                myMenuItem.hidden = true;
            }
        }
    </script>
    <!-- Feature XML -->
    <Feature
        Id="{70C245B0-5492-45c8-A1C6-BE6AB16159F2}"
        Title="JavaScript Hide Menu Items"
        Description="Przyklad ukrywania menu za pomoca JavaScript"
        Scope="Site"
        Hidden="FALSE"
        xmlns="http://schemas.microsoft.com/sharepoint/">
          <ElementManifests>
            <ElementManifest Location="elements.xml" />
          </ElementManifests>
    </Feature>
    
    <!-- Elements XML -->
    <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
      <Control
        Id="JavaScriptHideMenuItemsId"
        ControlSrc="~/_controltemplates/JavaScriptHideMenuItems.ascx"
        Sequence="100">
      </Control>
    </Elements>

Jeżeli pomysł się wam podoba, to by chować pozostałe elementy, należy dowiedzieć się o ich ID. Jest to dość łatwe do rozpoznania. Albo za pomocą IE Developer Toolbar lub nawet za pomocą widoku źródła strony. Wyszukujecie tekst menu, który was interesuje, na przykład Export To Spreadsheet, powinniście natrafić na następujący element:

<ie:menuitem id="zz17_ExportToSpreadsheet" type="option" iconSrc="/_layouts/images/MenuSpreadsheet.gif" ...

Nasze ID to to, które zostało zaznaczone na żółto.

Niestety, rozwiązanie to nie zadziała na elementy menu kontekstowego od elementu. Dzieje się tak, gdyż menu kontekstowe tworzone jest za pomocą kodu JavaScript a nie opisu menu w plikach XML czy w kodzie .NET.

To co ja osobiście robiłem (ale tego nie zalecam) kiedy musiałem zarządzać elementami menu kontekstowego, to edycja pliku CORE.JS. Za pomocą odpowiednich modyfikacji udostępniałem niektóre elementy a niektóre całkowicie ukrywałem.

Liam Cleary poszedł o krok dalej i przetestował kilka dodatkowych opcji zarządzania elementami menu kontekstowego. Wszystkie swoje spostrzeżenia oraz metody opisał w pisie MOSS2007 – Item Level Menus (Investigation).

Zarządzanie Menu Items za pomocą kodu .NET

Bardzo dobrym przykładem zarządzania menu items z poziomu kodu .NET jest rozwiązanie dostępne na CodePlex w projekcie Features: Toolbar Manager (link do opisu rozwiązania).

Innym rozwiązaniem, które sam nie raz stosowałem i nie tylko do menu items, ale także do innych elementów takich jak przycisk Edit Item na stronie Display Form, jest metoda opisana na Dipper Park.

Ja osobiście robie to za pomocą chowania elementów menu gdy użytkownik nie spełnia wymagań niezbędnych uprawnień do tego by wykonać akcję. Już w dwóch projektach miałem tak iż musiałem zachować uprawnienia Contributor dla grupy użytkowników, przy czym nie mogłem im udostępnić przycisku New na stronie listy. W tym celu korzystam z kontrolki SharePoint:

<Sharepoint:SPSecurityTrimmedControl runat="server" Permissions="ManageWeb">
</Sharepoint:SPSecurityTrimmedControl>

Wystarczy, że dany menu item umieści się wewnątrz tej kontrolki a nie wyświetli się ona do póki osoba nie spełni wymagań uprawnień opisanych w atrybucie Permissions. Te uprawnienia nie są niczym innym jak uprawnieniami z atrybutu CustomAction @Rights. Czy ich pełną listę można zobaczyć tutaj: SPBasePermissions Enumeration (Microsoft.SharePoint), pamiętajcie tylko, że uprawnienia można łączyć przecinkiem. Przecinek traktowany jest jak operator logiczny AND, czyli osoba musi mieć każde z wymienionych uprawnień by obejrzeć ten element.

Dla przykładu, jeżeli chcemy spowodować by ActionMenu pojawiało się tylko osobą z uprawnieniami ManageWeb, to następujący element:

<SharePoint:ActionsMenu AccessKey="<%$Resources:wss,tb_ActionsMenu_AK%>"runat="server" />

Zamieniamy na:

<Sharepoint:SPSecurityTrimmedControl runat="server" Permissions="ManageWeb">
	<SharePoint:ActionsMenu AccessKey="<%$Resources:wss,tb_ActionsMenu_AK%>"runat="server" />
</SharePoint:SPSecurityTrimmedControl>

Przy kontrolkach w 12 HIVE/TEMPLATES/CONTROLTEMPLATES należy wspomnieć o tym iż możemy wyciągnąć taką kontrolkę (od Tagu otwierającego <SharePoint:RenderingTemplate>do Tagu zamykającego </SharePoint:RenderingTemplate>) z już istniejącego pliku, zapisać ją w nowym pliku z naszymi zmianami (pamiętając o skopiowaniu nagłówków z rejestracją tag prefix). Jednakże trzeba pamiętać iż ta zmiana tyczy się zmiany globalnej. Czyli wszędzie gdzie te kontrolki są ładowane (a są na wszystkich stronach), ta zmiana będzie miała miejsce.

Wykorzystanie zaś Toolbar Manager, umożliwi nam kontrolowanie miejsc w których chcemy zarządzać wyświetlaniem menu. Toolbar Manager działa na zasadzie naszego Content Editor Web Part. Dodajemy go do widoku listy tam gdzie chcemy zarządzać elementami menu Toolbar. Różnica pomiędzy .NET code a JavaScript jest taka, iż JavaScript nie mogliśmy umieścić w Content Editor Web Part jeżeli chcieliśmy zarządzać elementami menu Toolbar, zaś mogliśmy zarządzać elementami menu Site Actions itp. W kodzie .NET zaś możemy zarządzać tym i tym :)

Stworzenie zarządzania elementami menu nie jest super trudne. Wystarczy przeciążyć funkcjęOnPreRender naszego WebPart w której następnie będziemy przechodzić po wszystkich kontrolkach na stronie i w zależności od typu kontrolki zaczniemy wykonywać odpowiednie akcje.Dla przykładu kod z Toolbar Manager:

private void ExamineControls(Control parentControl)
{
    foreach(Control control in parentControl.Controls)
    {
        if(control.ToString().ToUpper() == "Microsoft.SharePoint.WebControls.NewMenu".ToUpper())
        {
            NewMenu menu = (NewMenu)control;
            try
            {
                menu.Visible = this.ShowNewMenu;
            }
            catch{}
            try
            {
                menu.GetMenuItem("NewFolder").Visible = this.ShowNewFolder;
            }
            catch{}
        }
        // Kod wyciety...
        this.ExamineControls(control);
    }
}

Dzięki własnemu WebPartowi możemy także określać jego parametry – tak jak to robi Toolbar Manager. Dzięki czemu dajemy użytkownikowi końcowemu kontrolę nad tym co ma być a co nie wyświetlane. Daje do duży plus.

Oczywiście, tą samą metodą można także chować inne elementy menu – Site Actions czy Personal Menu. Dla przykładu, by ukryć menu w Site Actions możemy wykonać następującą modyfikację w kodzie od Toolbar Managera:

private void ExamineControls(Control parentControl)
{
    foreach(Control control in parentControl.Controls)
    {
        if(control.ToString().ToUpper() == "Microsoft.SharePoint.WebControls.SiteActions".ToUpper())
        {
            SiteActions menu = (SiteActions)control;
            try
            {
                menu.Visible = this.ShowSiteActionsMenu;
            }
            catch{}
            try
            {
                menu.GetMenuItem("MenuItem_EditPage").Visible = this.ShowEditPage;
            }
            catch{}
        }
        // Kod wyciety...
        this.ExamineControls(control);
    }
}

Oczywiście w ten sam sposób możemy dodawać kolejne elementy do menu, ale to tym była już mowa. Oczywiście ma to swoje ograniczenie – działa tylko i wyłącznie na stronie na której jest umieszczony nasz WebPart.

Możemy więc rozszerzyć naszą funkcjonalność o wszystkie strony w zależności od definicji Feature. Różnica polega na tym iż będziemy musieli zdefiniować od góry elementy które powinny zostać wyświetlone i kiedy oraz elementy które powinny zostać dodane. W tym celu możemy stworzyć kontrolkę już wcześniej opisaną w Zarządzaniu Menu Items za pomocą kodu JavaScript. Wgrać ją i cieszyć się naszą zmianą na wszystkich stronach.

Informacja dodatkowa

Stwierdziłem, że dodam tą informację, gdyż często pisałem o tym, iż jakieś rzeczy nie będą mogły być konfigurowalne i z góry będziemy musieli zakładać pewne działania – jak chowanie menu, czy jego dodawanie itp. itd. Jest to nie prawdą gdy tworzymy kod za pomocą .NET.

Do naszej dyspozycji jest WIELE metod konfiguracyjnych, do których możemy zaliczyć:

  • WebConfig – konfigurację naszą na podstawie pliku Web Config;
  • Database – konfigurację na podstawie informacji w naszej lub w bazie danych SharePoint;
  • Custom List – konfigurację na podstawie informacji zwartych na naszej liście konfiguracyjnej;
  • Hierarchical Object Store – konfiguracja trzymana w informacji na temat strony, wSPPersistedObject. Więcej na temat Hierarchical Object Store można znaleźć tutaj. Do jej zarządzania istnieje Feature Manage Hierarchical Object Store dostępny w pakiecieFeatures na stronie CodePlex.

Dodatkowe opisy do sposobu przechowywania konfiguracji można znaleźć w artykule Creating Custom Timer Jobs in Windows SharePoint Services 3.0 w punkcie Configuration Options. Wszystko więc zależy od waszej kreatywności. Jeżeli chcecie stworzyć własną stronę do zarządzania menu to nic nie stoi wam na przeszkodzie. Jedyna kwestia to czas :) Ale jak już raz coś takiego stworzycie to potem będziecie mogli to wykorzystywać wszędzie.

Podsumowanie

W postcie, który wstępnie miał traktować o prostym stworzeniu pogrupowanego menu w Site Action, zostały zawarte informacje, które moim zdaniem dogłębnie opisują większość metod tworzenia własnych menu w SharePoint powiązanych z Custom Actions. Takie elementu menu jak dodatkowe pola w WebPart czy samo tworzenie dodatkowych elementów w Web Part ToolPart nie zostały poruszone jak i też odbiegają one od Custom Actions. Jak sami zauważyliście sposobów tworzenia jest wiele, każdy ma swoje plusy jak i minusy oraz ograniczenia. To o czym jeszcze warto tutaj wspomnieć to zasięg i ograniczenie poszczególnych metod tworzenia własnych menu:

Metoda Zasięg Ograniczenie
JavaScript Poprzez modyfikację CORE.JS nasze menu będzie dostępne na każdej stronie! Także można wykorzystać inne sposoby deklaracji JavaScript (patrz przykłady) które udostępnia dane modikację na daną pojedynczą stronę, na Web na Site itp. Dostępne jedynie dla kontekstowego menu elementu na listach.
Feature Zasięg określany jest przez atrybut Scope feature. Opis elementu Feature na MSDN oraz opis elementu Scope na MSDN. Dostępne we wszystkich możliwych elementach do których można wgrać własne menu.
Własny Kod Tak jak i Feature, nasze menu wgrywane jest przez feature, przez co Scope definiuje ograniczenie dostępności naszego menu. Także dodatkowym ograniczeniem jest nasz Custom Code, który może zdefiniować warunki kiedy element ma się wyświetlić.

Za pomocą własnego kodu nie ma możliwości stworzenia menu dla elementu na liście. Kod jest całkowicie pomijany i nie jest wywoływany. Dla testu możecie stworzyć sobie kod, który na CreateChildControls wywoła Exception. Zobaczycie, że on nigdy nie zostanie wywołany. Dodatkowo post Use of Assembly for a CustomAction in the EditControlBlock region? tłumaczy chyba wszystko. Dodatkowo metoda: internal static string RenderECBItemsAsHtml(SPWeb web, SPList list) w klasieSPCustomActionElement nawet nie sprawdza innych parametrów XML niż te które definiują obrazek, akcję i tekst.

Tak jak kontekstowe menu, także nie można dodać elementu do menu administracyjnego – też nie jest wywoływany kod zadeklarowany w ControlAssembly i ControlClass.

Kontrolka Tak jak i Feature i Własny Kod. Instaluje się tak samo i także poprzez podanie Scope definiuje się ograniczenie elementu. Trzeba modyfikować Master od strony, czyli moim zdaniem znaczące – nie lubię ingerować w Master chyba, że udostępniam swój własny.
Zarządzanie przez JavaScript Dowolny, w zależności od sposobu wgrania JavaScript, elementy można ukrywać globalnie, stronnicowo lub elementowo. Kłopot z zarządzaniem z kontekstowym menu elementu. Bardzo dobry wpis na ten temat można przeczytać tutaj: MOSS2007 – Item Level Menus (Investigation). Także zarządzanie menu administracyjnym, może stanowić problem.
Zarządzanie przez kod .NET Tutaj mamy swobodę w zarządzaniu. Możemy stworzyć własną kontrolkę, która podobnie jak Toolbar Manager będzie działała na zasięgu strony i tyczyła wszystkich menu, czy wgrać Control tak jak w zarządzaniu JavaScript i mieć kontrolę na wszystkich stronach, czy też stworzyć swój własny system zarządzania menu, który umożliwi użytkownikowi na decydowanie co, gdzie i jak ma się wyświetlić. Jak już wiadomo, nie mamy jak ukrywać elementów menu kontekstowego :( Jednak zarządzanie przez kod .NET umożliwia nam kontrolę wyświetlania każdego elementu menu poza elementem kontekstowego menu. W zależności od tego co napiszemy i jak to zainstalujemy, będziemy mogli zrobić prawie wszystko – nie możemy zarządzać menu administracyjnym (w sensie nie będzie to łatwe by coś ukryć lub dodać).

Jeszcze na sam koniec, należy zaznaczyć iż nie wszystkie skrajne warunki zostały poruszone. Poprzez analizę JavaScript, schematów XML oraz kodu SharePoint na pewno można wyłapać jeszcze wiele zależności pomiędzy elementami. Na przykład kiedy element menu nie zostanie wyrenderowany i dla czego, co do tego doprowadza itp. Dlatego jak wam coś nie pójdzie z instalacją własnych menu to sprawdźcie najpierw czy u was wszystko gra, a jeżeli gra, to pora zajrzeć w źródła SharePoint – może coś powinno zostać wykonane inaczej? A może tego po prostu się nie da zrobić.

Wszystkich chętnych zapraszam do komentowania. Jeżeli czegoś nie poruszyłem w postcie dajcie znać a na pewno go zaktualizuje.

Zasoby

Na wszelki wypadek wypisuje tutaj ponownie wszystkie zasoby, które zostały użyte w treści postu:

UWAGA:

Ze względu na to iż nie można publikować dwóch załączników do tekstu, tutaj macie linki do określonych plików:

11 KOMENTARZE

  1. No to się nazywa book blog. Dawno nie widziałem tak długiego posta w polskiej blogosferze. A sobie poczytać aż miło że się tak wyraże.

  2. Tekstu są dokładnie 34 strony. Gutek, przyznaj się, że to tak naprawdę rozdział z Twojej książki o MOSS :), bo to, że jest to najdłuższy post, jaki kiedykolwiek pojawił się na Zine – to jest pewne. Aż się boję spytać, ile Ci zajęło spisanie tego wszystkiego.

    Szkoda, że nie mogę dodać żadnego merytorycznego komentarza, bo na MOSS się po prostu nie znam, ale z mojej strony wyrazy uznania za pracę, jaką włożyłeś w przygotowanie tego tekstu.

  3. No Gutek, pokazałeś klasę:). Póki co z MOSS nie muszę mieć do czynienia, ale jak będę musiał to…
    Gdyby KIEDYŚ miała powstać kolejna wersja ITCore to sam pewnie w tydzień natrzaskasz:).
    I jeszcze z ciekawości: ile z tego tekstu powstało w zeszłym tygodniu gdy miałeś milion stopni gorączki i ogień w skrzelach? ;)

  4. Huh z takim zacięciem może uda się przełamać stereotyp MOSS jak trudnej platformy do developmentu .. :) gr8 job ;D

  5. @apl

    ;) szczerze, dwu krotnie popelnilem to przestepstwo i juz trzeci raz raczej nie bede probowal ;)

    @Procent

    …tak tak ;) dzwon smialo ;) Co do goraczki… to jakas 1/2 ;) wiec moga wystapic jakies zagmotwane wywody, w ktorych wiekszosc sie gubi (nawet ja) ;)

    co do itcore v3.0… no bo ja wiem ;) moje podejscie do technologii wss/moss sie nie zmienilo, wiec raczej zadzwonie do Ciebie i Binka po pomoc ;)

    @arkadiusz

    Dzieki :)

    @Bysza

    ksiazki juz nie popelnie ;) mam przynajmniej taka nadzieje

    @woro

    dzieki :)

  6. Hi Chris,

    I’ve send you a mail, please let me know if you get it (just in case if you use outlook, take a look into Junk Mail folder).

    Cheers,
    Jakub G

Comments are closed.