Sharepoint, MOSS un WSS – 2007. versija

Šķiet, esmu viens no retajiem LV, kuram šīs lietas vispār interesē, tomēr pastāstīšu – lai jau saglabājas nākotnei.

Kopš šī gada sākuma tīmeklī it bieži tiek pieminēts “jaunais Sharepoint” – visvairāk kā apjūsmojoši ieraksti dažādos blogos, cik ērts gan tas būs. Runa ir par Sharepoint 2007, precīzāk, diviem Sharepoint saimes produktiem – Windows Sharepoint Services (WSS) 3.0 un Microsoft Office Sharepoint Server (MOSS) 2007. Ar ko tie atšķiras? Apmēram ar to pašu, ar ko Notepad atšķiras no Wordpad – ar iespēju bagātību. Ja WSS iedomājamies kā skudrupūzni, tad MOSS ir skudrupūznis ar centrālapkuri, četriem skursteņiem un peldbaseinu pagrabā. Vārdu sakot, MOSS ir daudz papildus lietu – unificēta meklēšanas sistēma, lietotāju individuālie saiti, papildus saitu templeiti, papildus darba plūsmu realizācijas, biznesam svarīgo indikatoru (KPI, key performance indicators) monitorēšana, Excel web servisi utt utt. Vēl viena atšķirība – WSS ir “par velti”, tas ir, kopā ar Windows Server 2003, bet MOSS ir maksas produkts… un dārgs maksas produkts.

Atpakaļ pie pamatstāsta. Solīja jau kādu laiku, šī gada sākumā iznāca pirmā beta versija, kas bija patiešām ar īstu “betas” garšu, šī gada maijā – otrā Beta, augustā – Beta2TR versija. Pagājušajā nedēļā Microsoft paziņoja par galaversijas iznākšanu, pašlaik jebkuram ir pieejama 180 dienu trial versija MOSS un, protams, jebkuram Windows Server 2003 īpašniekam – WSS 3.0 RTW Instalēšanai būs noderīgs .NET Framework 3.0, kas arī tika izziņots pagājušajā nedēļā.

Par jaunumiem šajā versijā. Vēl joprojām Sharepoint kā datu glabātuvi izmanto Microsoft SQL Server, tagad varam lietot gan 2000., gan 2005. versiju. Joprojām tas darbojas kā IIS webservera process (bet, atšķirībā no iepriekšējās versijas, tagad kā normāla ASP.Net aplikācija, nevis ISAPI filtrs). Ir krietni uzlabota Active Directory integrācija, tagad varam precīzāk definēt attiecību “domēna lietotājs:portāla lietotājs”, piemēram, nosakot, kurus portāla lietotāja atribūtus nolasīt no kuriem domēna lietotāja konta atribūtiem (šī atkal laikam tikai mani interesējoša nianse).

Piekļuves tiesību granularitātes līmenis (ak, svešvārdi…). 2003. versijā nebija daudz iespēju – piekļuvi varēja definēt vai nu visam saitam kopā, vai katrai dokumentu bibliotēkai vai sarakstam atsevišķi. Un viss. Jaunajā variantā piekļuves tiesības varam definēt līdz pat ieraksta līmenim, kas ir daudz loģiskāk, ja nepieciešams izveidot koplietošanas dokumentu glabātvi. Šī iemesla dēļ gan sanācis tā, ka tiesību administrēšana ir kļuvusi ķēpīgāka. Ja līdz šim šķita, ka normālā prakse būtu – 1) Sharepoint saita līmenī sadefinēt loģiskas lietotāju grupas 2) Šīm grupām piešķirt tiesības skatīt to vai citu dokumentu bibliotēku, sarakstu, darīt to vai citu darbību 3) Šajās grupās ievietot cilvēkus vai grupas no domēna … tad tagad rodas sajūta, ka “ai, vai tad nu es šī viena dokumenta dēļ taisīšu speciālu grupu?”, tātad tiesības tiek izmētātas pa visu saitu.

Audiences (šķiet, šī ir MOSS, nevis WSS iespēja). Katram ierakstam varam norādīt mērķauditoriju (target audiences), t.i., grupas, kam tas varētu būt interesants. Tāpat – lapā ievietotam webpart-am var norādīt auditoriju. Ja pareizi saprotu, piekļuve šādā veidā netiek aizliegta, bet attiecīgais ieraksts vai webparts tiek aizvākts no to lietotāju acīm, kam tas nebūs vajadzīgs. Un tas ir ērti.

Navigācijas sistēma. Jau noklusētajā shēmā tā ir stipri sakarīga. 1) katram saitam var veidot savu augšējo navigācijas paneli, sastāvošu no atsevišķām cilnēm (tabs). Ir izvēle, vai mantot “parent” saita tabus, vai izmantot savējos. 2) stipri labāks ‘quick launch’ menu saita kreisajā malā. Ir izvēle, vai nu automātiski ģenerēt no visām dokumentu bibliotēkām un sarakstiem, vai veidot pašam savējo 3) breadcrumbs pasākums, kas parāda vertikālo navigāciju saitu struktūrās. Visumā ir sajūta, ka apmaldīties nevarēs.

Darba plūsmu programmēšana. Kopā ar Windows Workflow Foundation ir atnākusi iespēja strādāt ar darba plūsmām arī no Sharepoint. Ir sajūta, ka te ir savāktas vienkāršākās un nepieciešamākās lietas no Biztalk Server un palaistas plašākās tautās. Līdz ar to tādi procesi kā dažādu veidu vīzēšana, atsauksmju savākšana, atbilžu sagatavošana un citi darbiņi, ko cilvēki veic nelielās komandās, ir aprakstāmi un programmējami arī šajā vidē.

Multiple lookups. Iepriekšējā Sharepoint versijā sarakstam bija iespējams izveidot lauku, kurš norāda uz cita tajā pašā saitā esoša saraksta konkrētu ierakstu. Piemēram, cilvēku tabulā varētu būt lauks, kas ir norāde uz struktūrvienību sarakstu. Līdz šim nebija iespējams apskatīt situāciju, kad cilvēks strādātu vairākās struktūrvienībās vienlaicīgi. Tagad ir.

Custom field types. Šis ir interesants. Pēc noklusējuma Sharepoint sarakstiem var pievienot laukus ar tipiem kā “text”, “multiline text”, “number”, “date”, “choice”, “lookup”… tagad varam veidot savus tipus. Nu, kaut vai tas pats iecienītais piemērs ar personas kodu – zināms teksta formāts, zināms garums. Atliek tikai izstrādāt savu klasi, kas mantota no SPField tipa, izveidot rediģēšanas kontroli savam lauka tipam (Jo Sharepoint taču automātiski ģenerē datu ievades un parādīšanas formas katram sarakstam), varbūt vēl neliela čupiņa koda… un tam ir jādarbojas. Pagaidām neesmu izmēģinājis, bet jau ticu, ka būs labi.

Wiki un Blog saitu templeiti. Nezinu, pagaidām nešķiet, ka jaunā spēļmantiņa būs tik vērta, cik viņi paši sola. Wiki sintakse ne tuvu nelīdzinās wikipedia iespējām, blogi it kā ok, pat komentēt var… bet kaut kā nav ērti, vismaz ne priekš tiem, kas paši tajos raksta.

Overall – labais! Uzrakstīšu citreiz vēl, šoreiz jau tā par garu sanāca.

Always, ALWAYS initialize!

What output would you expect from such a piece of code (VB.net, of course):

    Imports System.Globalization
    .... 
    Dim CurrentCalendar As System.Globalization.Calendar = _
                CultureInfo.CurrentUICulture.Calendar

    'We want to output all the dates in the current year for every month.
    For iMonth As Integer = 1 To 12
        Dim sOutput As String
        'accumulate all the dates
        For iDay As Integer = 1 To CurrentCalendar.GetDaysInMonth(Now.Year, iMonth)
            sOutput &= iDay & ","
        Next
         'print out all the accumulated dates
        Console.WriteLine(sOutput.TrimEnd(","c))
    Next

I’d say it must be

1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31

but, the hell, no, it is:

(1st line)
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31
(2nd line)
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,1,2,3,
                   ...other Februray dates..28
(3rd line)
...January dates...February dates...March dates...
(4th line)
...January dates...February dates...March dates...April dates...
(5th line)
...January dates...February dates...March dates...May dates...   
....
(12th line)
...January dates...February dates...March dates...May dates...December dates...

This means that even though the variable s is re-declared, it is not being re-initialized! Just… keep it in mind.

Hint: instead of being Dim sOutput As String it should be Dim sOutput As String="" if we still want to keep this syntax.

IsUserInGroup updated

In an earlier article I’ve been talkin about how to check if a user belongs to a specific domain group. However, the code in that article doesn’t work on local groups and WinNT ADSI provider. I’ve made an update to the code posted there, this should work both on domain and local groups.

Usage:

If IsUserInGroup("Contoso\JohnDoe", "Contoso\Managers") Then
   '...do something
End If

Notes:

  • Option Strict Off. Sorry, I’m using the GetObject magic function here, so there’s no way I could leave the option strict on.
  • Seems to be quite slow when local groups are scanned from outside.
  • Could be a problem when you have users from two different domains + local users.

    Continue reading IsUserInGroup updated

cmdSave.Enabled = False

Nemaz nav prātīgi domāt, ka, ASP.Net kontrole (piemēram, poga), kas it kā ir “atspējota” (disabled, not enabled), patiesībā tāda ir. Vienīgais, ko patiesībā ASP.Net izdara, ir, ģenerējot HTML kodu, kontroles kodā ieraksta disabled=true.

Taču, ja šo pogu klienta pusē atkal “iespējojam” (eneiblojam), piemēram, ar šāda te:

javascript:( 
    function(){
      d=document;
      l=d.getElementsByTagName("INPUT");
      for(var i=0;i<l.length;i++){
         l[i].disabled=false}
 }())

JavaScript bookmarkleta palīdzību, ASP.Net neveiks nekādas servera puses pārbaudes, vai poga ir bijusi ieslēgta, vai nav. Tas jādara pašiem. Vislabāk nemaz nedot lietotājam rokās pogu, kuru nevar nospiest, bet, ja gribas saglabāt skaistu interfeisu, tad cmdSave_Click(...) kods jāpapildina ar pārbaudi, vai poga šobrīd vispār ir ieslēgta.

Paldies par uzmanību :-)

Finding SPListItem by its ID

I just found out that some operations in my SharePoint application run quite slow when used on lists containing large amounts of listitems. In the particular case it was approximately 2,500 items in the list.

The result is: NEVER use this code construction to find an element by its ID:

oList = oWeb.Lists("LongList")         'gets the list object
oItem = oList.Items.GetItemById(123)   'finds the element within the list in the list

You should rather use this one:

oList = oWeb.Lists("LongList")         'gets the list object
oItem = oList.GetItemById(123)         'finds the element within the list in the list

The big mistake is that accessing the “Items” property of the SPList object oList creates an SPQuery object in background to read all the items in particular list and then GetItemById() method of the SPListItemCollection (oList.Items is a SPListItemCollection object) runs a loop, looking for an item with ID attribute set to the value 123.

However, if you use the second code example the SPList ojbect oList creates an SPQuery object in background to read only one object having ID attribute set to 123, i.e.:

<Where><Eq><FieldRef Name="ID"></FieldRef><Value Type="Integer">123<Value></Eq></Where>

and then returns the element it found.

The same applies to the DeleteItemById() method. I had to work around in a bit different manner:

'oList.Items.DeleteItemById(123) 'this was the original code
oList.GetitemById(123).ListItems.DeleteItemById(123) 'this is the improved code

As you can see, the improved code first finds the list item by its ID, then accesses the ListItems property (which is again SPListItemCollection) and executes a DeleteItemById() command on the particular collection where the size of the collection is 1.

A really difficult workaround was needed to fix the AddNewItem function where I had code like this:

Dim oNewItem as SPListItem
oNewItem = oList.Items.Add()

Again, accessing the Items attribute leads to reading all the items in the list. To avoid that, I created a SPQuery object that never returned any elements, then called Add() method on the collection returned by the query.

'reads all items having id=0. Should return no rows
oEmptyQuery.Query = "<Where><Eq><FieldRef Name='ID' />" & _
        "<Value Type='Counter'>0</Value&gt;</Eq></Where>"
oItemColl = oList.GetItems(oEmptyQuery)
oNewSPItem = oItemColl.Add()    'adds an item to the empty list
'saves the new item and re-reads it from SharePoint
oNewSPItem.Update()
oNewSPItem = oList.GetItemById(oNewSPItem.ID)

The two last lines were needed because it seems that the field collection is not geting populated when a query returns no rows. However, my code didn’t seem to run nice without the last two rows.

SPUser? SPException!

Something on SharePoint(TM) again. Either you love it or hate it, you have to work around all the pitfalls anyway.

So we all know that in SharePoint you can allow people and assign them SharePoint roles in two ways:

  • creating a “Sharepoint User” within the portal and assigning roles to the user,
  • adding a domain group to users and assigning roles to a particular group.

The first case is the easy one. The second is not. Continue reading SPUser? SPException!

Does this user belong to that domain group?

UPD: This works only on LDAP groups and LDAP:// provider, not WinNT groups!

If we know two things – a login name of a user for instance northwind\JohnDoe and a domain group northwind\board. We want to check if John Doe belongs to the “board” group or not.

This is how you find it: IsUserMember( "northwind\JohnDoe", "northwind\board")

Continue reading Does this user belong to that domain group?

Calculated fields in CAML

So you have a SharePoint list with a calculated field. You want to select items based on the calculated field beginning with some specific substring. You write a CAML query:

<Where>
  <BeginsWith>
     <FieldRef Name="SortDate" />
     <Value Type="Calculated">200509</Value>
  </BeginsWith>
</Where>

This won’t work. (At least on my server) you’d always get informed that ” The SQL Server might not be started” + get a useless COM exception number 0x81020024. You can work around the problem by changing the value type to “Text”

<Where>
  <BeginsWith>
     <FieldRef Name="SortDate" />
     <Value Type="Text">200509</Value>
  </BeginsWith>
</Where>

This also works on the “Contains” operator. Other operators such as LessThan, GreaterThan, Equals, IsNull allow you to set the real value type – Calculated. Don’t know why. Probably a bug in Sharepoint.

Month names. i18n, you know

Whenever you see something like this you should come to an idea something is wrong (for instance, the guy who wrote this is still alive):

'month names in Latvian
Dim sMonth() As String = _
            {"Janvāris", "Februāris", "Marts", "Aprīlis", _
            "Maijs", "Jūnijs", "Jūlijs", "Augusts", _
            "Septembris", "Oktobris", "Novembris", "Decembris"}

Dim oListItem as ListItem
For iMonth as Integer = 1 To 12
  oListItem = New ListItem(sMonth(iMonth-1),  iMonth.ToString())
  Me.lstYears.Items.Add(oListItem)
Next

Explanation – if MS Windows knows quite well how months are named in Latvia, why should you try to be smarter than the big brother? Another explanation – if this is a web project (and in my case it is), you should think of internationalization whenever possible.

So a more correct way to add month names to a dropdown menu is:

Imports System.Globalization
....
Dim oListItem as ListItem
For iMonth As Integer = 0 To 11
   oEntry = New Web.UI.WebControls.ListItem( _
         CultureInfo.CurrentUICulture.DateTimeFormat.MonthNames(iMonth), _
            iMonth.ToString)
   Me.lstMonths.Items.Add(oEntry)
Next

And, to be even more precise, you could first check how many months there are in the current culture:

For iMonth As Integer = 0 To _
                CultureInfo. CurrentUICulture.Calendar. _ 
                GetMonthsInYear(Now.Year) - 1
    ....

/Just trying to be a good boy and use the facilities provided by the framework/

Parse the Enum!

What if we need to save the value of an instance of an enumeration (Enum) as string and then get back the value again?

For instance, we have the following code:

Dim eDay As System.DayOfWeek = DayOfWeek.Monday

So now we can get a textual representation of eDay using the built-in ToString() method:

MessageBox.Show(eDay.ToString())

This yields a messagebox saying “Monday”. But how do we do the reverse (i.e., we know only the string representation “Monday”, but we need the enum value)? The trivial approach would be:

    Dim sStr As String = "Monday"
    Dim eDay As System.DayOfWeek
    Select Case sStr
        Case "Monday" : eDay = DayOfWeek.Monday
        Case "Tuesday" : eDay = DayOfWeek.Tuesday
            '...
    End Select

No, this sounds too dumb. We can use the built-in shared function Parse of the System.Enum class. Method accepts two parameters, the type of enumeration and the actual value being parsed.

eDay = System.Enum.Parse(GetType(System.DayOfWeek), "Monday")

This only works with option strict set to off, because Enum.Parse() returns a System.Object value. So when using option strict set to on, the code gets more obfuscated:

eDay = CType( _ 
          System.Enum.Parse(GetType(System.DayOfWeek), "Monday"),  _ 
          System.DayOfWeek)

Anyway, now we have the value! :)