WSS: Hromadné odstranění záznamů v listu

Pokud jste někdy potřebovali kompletně vyprázdnit list/knihovnu dokumentů, určitě jste rychle narazili, stejně jako já, na neexistenci takové funkce. Samozřejmě je možné mazat jednotlivé záznamy v iteraci, to je ale velice časově nákladná operace, kterou nejde použít ani na desítkách záznamů 🙁 Smazání jednoho záznamu trvá klidně i 500ms, 1000 záznamů by se tedy mazalo 5-10 minut.

Nakonec jsem našel trošku krkolomné řešení, které je však řádově rychlejší než mazání v iteraci. Základem celé akce je složení velkého XML balíku hromadného příkazu pro smazání jednotlivých záznamů v jednom volání. Vtip je v tom, že i když toto řešení také iteruje celým seznamem v listu, property ID je u SPListItem už přednačtena v cache Sharepointu, ten tedy nemusí pro každou iteraci sahat do DB, složení tak může proběhnout rychle. Výsledný balík se už jen předhodí enginu WSS, který jej interně zpracuje. Po ukončení ještě v mém případě volám vyprázdnění koše aktuálně přihlášeného uživatele, to samozřejmě není nutné a může být v některých případech i kontraproduktivní při volání pod „real-life“ uživatelem.

Pokud existuje lepší cesta, určitě bych se nechal rád inspirovat 🙂

private static void BatchListItemsDelete(SPList spList)
{
    StringBuilder sbDelete = new StringBuilder();
    sbDelete.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?><Batch>");
    string command = "<Method><SetList Scope=\"Request\">" + spList.ID +
        "</SetList><SetVar Name=\"ID\">{0}</SetVar><SetVar Name=\"Cmd\">Delete</SetVar></Method>";

    foreach (SPListItem item in spList.Items)
        sbDelete.Append(string.Format(command, item.ID.ToString()));

    sbDelete.Append("</Batch>");

    spList.ParentWeb.ProcessBatchData(sbDelete.ToString());
    spList.ParentWeb.RecycleBin.DeleteAll(); 
}

Zobrazení aktuálních aktivit Microsoft CRM 4 v Sharepointu + zdrojové kódy

Na tomto příspěvku jsem chtěl demonstrovat jednoduchost a vůbec možnost provázání různých aplikací s Sharepointem. Rozhodl jsem se pro vytvoření webparty, zobrazující aktuálně přiřazené a nesplněné aktivity v Microsoft CRM 4, protože to bude určitě užitečná featura pro spoustu lidí využívajících firemní intranet.

Takto vypadá výsledný webpart (po kliknutí na daný předmět aktivity se otevře přímo v editačním okně CRM):

 

Takto jsou zobrazeny aktivity v CRM:

Zdrojové kódy i WSP balíček je přiložen na konci tohoto postu.

Načítání aktivit z CRM serveru probíhá přes webservice, url adresa webové služby je vždy ve formátu:

http://server:port/MSCRMServices/2007/CrmService.asmx

Pokud používáte Visual Studio 2008, je potřeba vygenerovat klientský wrapper až v „Advance“ dialogu jako na následujícím screenshotu:

 

Aby bylo možné rozlišit aktuálně používanou organizaci zavedenou v CRM, je potřeba ještě provést nastavení objektu CrmAuthenticationToken jako na následujícím příkladu:

public static CrmService GetCrmService(string crmServerUrl, string organizationName)
        {
            if (string.IsNullOrEmpty(crmServerUrl)) throw new ArgumentNullException("crmServerUrl");
            if (string.IsNullOrEmpty(organizationName)) throw new ArgumentNullException("organizationName");

            CrmSdk.CrmAuthenticationToken token = new CrmSdk.CrmAuthenticationToken();
            token.OrganizationName = organizationName;

            CrmService service = new CrmService();
            UriBuilder builder = new UriBuilder(crmServerUrl);
            builder.Path = "//MSCRMServices//2007//CrmService.asmx";
            service.Url = builder.Uri.ToString();
            
            service.Credentials = System.Net.CredentialCache.DefaultCredentials;
            service.CrmAuthenticationTokenValue = token;

            return service;
        }

 

Aby jsme mohli vyhledat svoje aktivity, je nejprve nutné zjistit identifikátor přihlášeného uživatele:

WhoAmIRequest request = new WhoAmIRequest();
WhoAmIResponse response = (WhoAmIResponse)service.Execute(request);
Guid userId = response.UserId;

Pak už jen stačí vyhledat aktivity s přiřazeným vlastníkem na sebe sama a nastaveným status kódem – open nebo scheduled.

private static BusinessEntityCollection GetAssignedActivities(CrmService crmService, Guid userId)
        {
            // budeme nacitat aktivity prirazene prihlasenemu uzivateli
            ConditionExpression condition = new ConditionExpression();
            condition.AttributeName = "ownerid";
            condition.Operator = ConditionOperator.Equal;
            condition.Values = new object[] { userId };

            // potrebujeme pouze otevrene aktivity
            ConditionExpression condition2 = new ConditionExpression();
            condition2.AttributeName = "statecode";
            condition2.Operator = ConditionOperator.Equal;
            condition2.Values = new object[] { (int)ActivityPointerState.Open };
            ConditionExpression condition3 = new ConditionExpression();
            condition3.AttributeName = "statecode";
            condition3.Operator = ConditionOperator.Equal;
            condition3.Values = new object[] { (int)ActivityPointerState.Scheduled };

            FilterExpression filterState = new FilterExpression();
            filterState.FilterOperator = LogicalOperator.Or;
            filterState.Conditions = new ConditionExpression[] { condition2, condition3 };

            FilterExpression filterRequired = new FilterExpression();
            filterRequired.FilterOperator = LogicalOperator.And;
            filterRequired.Conditions = new ConditionExpression[] { condition };

            FilterExpression filter = new FilterExpression();
            filter.FilterOperator = LogicalOperator.And;
            filter.Filters = new FilterExpression[] { filterRequired, filterState };

            // seradime podle konce aktivity
            OrderExpression order = new OrderExpression();
            order.AttributeName = "scheduledstart";
            order.OrderType = OrderType.Ascending;
            
            // vytvoreni vyhledavaciho dotazu - potrebujeme vsechny aktivity
            QueryExpression query = new QueryExpression();
            query.EntityName = EntityName.activitypointer.ToString();
            query.ColumnSet = new AllColumns();
            query.Criteria = filter;
            query.Orders = new OrderExpression[] { order };

            // vykonani samotneho dotazu
            return crmService.RetrieveMultiple(query);
        }

 

 Následně již stačí vyrenderovat všechny položky do HTML. Prosté, ale dobré 🙂

Instalace feature:

cd C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\BIN

stsadm -o addsolution -filename crm4connector.wsp

stsadm -o deploysolution -name crm4connector.wsp -immediate -allcontenturls -allowGacDeployment -allowCasPolicies

následně aktivujte feature ve správě webu:

 

Přidejte feature „Activities in CRM“ na libovolnou stránku:

 

V nastavení feature je nutné ještě nastavit url adresu serveru s Microsoft CRM 4 a název organizační jednotky: 

(níže je také možné upravit obecné texty podle potřeby – podle aktuálně používaném jazyku)

 

Hotovo, po potvrzení by se měl zobrazit seznam aktivit aktuálně přihlášeného uživatele. Pokud je něco špatně, budete upozorněni:

Ke stažení:

sources.zip (272,14 kb)

bin.zip (244,01 kb)

Nastavení XML-RPC.NET programově bez nutnosti existence konfiguračního souboru

Jelikož se mi v aktuálním projektu nehodila existence konfiguračního souboru aplikace (app.config) pro zajištění konfigurace XML RPC serveru ve vlastní assembly, potřeboval jsem stejné parametry nastavit programově.

Nakonec jsem narazil na odpověď:

ListDictionary prop = new ListDictionary();

prop.Add("port", 1971);

HttpChannel channel = new HttpChannel( prop, null, new XmlRpcServerFormatterSinkProvider(null, null));

ChannelServices.RegisterChannel(channel);

RemotingConfiguration.RegisterWellKnownServiceType( typeof(Server), "myApp/anURI", WellKnownObjectMode.Singleton);

Blog přesunut

Blog se mi konečně podařilo přesunout na vlastní doménu pavelnovotny.info, respektive se mi podařilo dokopad sebe sama, prozkoumat alternativy blogovacích nástrojů a pustit se do toho. Nakonec jsem použil BlogEngine.NET, hlavně pro svoji rychlou a snadnou instalaci, konfiguraci, skinovatelnost – vše bez nutnosti hlubokého zkoumání dokumentace, šlo to tak nějak samo.

Programové nastavení UseUnsafeHeaderParsing

Vzhledem k častému rozpadání spojení při volání některých java webservices serverů je nutné buď vypnout keep alive nebo nastavit useUnsafeHeaderParsing. První metoda výrazným způsobem ovlivní výkon, protože se při každém requestu znovu navazuje tcp spojení, druhá se zase doporučuje pouze u „bezpečných“ serverů, u kterých nehrozí nebezpečí útoků pomocí přetečení bufferu.

V případě projektů, při kterých je i server v naší režii, je určitě lepší cesta nastavením useUnsafeHeaderParsing, to je možné jak v app.config, tak i programově pomocí reflexe:

Nastavení v app.config:

<system.net>
<settings>
<httpWebRequest useUnsafeHeaderParsing = "true"/>
</settings>
</system.net>

Programové nastavení pomocí reflexe:

[more]

public static bool SetAllowUnsafeHeaderParsing()
        {
            Assembly aNetAssembly = Assembly.GetAssembly(
              typeof(System.Net.Configuration.SettingsSection));
            if (aNetAssembly != null)
            {
                Type aSettingsType = aNetAssembly.GetType(
                  "System.Net.Configuration.SettingsSectionInternal");
                if (aSettingsType != null)
                {
                    object anInstance = aSettingsType.InvokeMember("Section",
                      BindingFlags.Static | BindingFlags.GetProperty
                      | BindingFlags.NonPublic, null, null, new object[] { });
                    if (anInstance != null)
                    {
                        FieldInfo aUseUnsafeHeaderParsing = aSettingsType.GetField(
                          "useUnsafeHeaderParsing",
                          BindingFlags.NonPublic | BindingFlags.Instance);
                        if (aUseUnsafeHeaderParsing != null)
                        {
                            aUseUnsafeHeaderParsing.SetValue(anInstance, true);
                            return true;
                        }
                    }
                }
            }
            return false;
        }

Více zde: http://msdn.microsoft.com/en-us/library/system.net.configuration.httpwebrequestelement.useunsafeheaderparsing.aspx

DirectorySearcher vrací maximálně 1000 záznamů

MSDN dokumentace neobsahuje příliš mnoho informací k tomuto problému, naštěstí v diskustní skupině to již někdo řešil.

Objekt DirectorySearcher obsahuje dvě klíčové vlastnosti:

PageSize – defaultně nastaveno na 0 – bez stránkování, určuje počet záznamů v jednom interním requestu na server, o „stránkování“ se stará sám objekt vyhledávače.

SizeLimit – defaultně 1000, maximální počet vrácených záznamů

Při defaultním nastavení nikdy nevrátí více než 1000 záznamů, protože v tom případě vyhledávač nestránkuje. Pokud jste si mysleli, že stačí SizeLimit změnit na vyšší hodnotu, tak jste se spletli. Snad jediná možná cesta je změnit PageSize, aby došlo ke stránkování, ale zde je další vtip dokumentace, nikde nenajdete, ze PageSize musí být z intervalu 0-1000 a už vůbec to, že pokud nastavíte hodnotu 1000, opět nedojde ke stránkování a DS vrátí pouze 1000 záznamů – to považuji snad za bug… Správna cesta je nastavit PageSize na menší hodnotu, například 999, následně již obdržíte záznamy všechny…

Příklad načtení všech uživatelů z LDAPu:

DirectorySearcher mySearcher = new DirectorySearcher(rootEntry);

mySearcher.Filter = "(&(objectCategory=Person)(objectClass=User))";
mySearcher.PropertiesToLoad.Add("cn");
mySearcher.PropertiesToLoad.Add("objectClass");
mySearcher.PropertiesToLoad.Add("sAMAccountName");
mySearcher.PageSize = 999;

SearchResultCollection mySearcherSearchResult = mySearcher.FindAll();

Zobrazení call stack při chybě

Sharepoint standardně při vzniku exception zobrazuje pouze její message, nikoliv i kompletní call stack, pro zobrazení všech detailů je potřeba modifikovat web.config následovně:

 

<customErrors mode="Off" />

<compilation debug="true">

 a ještě:

<SafeMode MaxControls="200" CallStack="true" DirectFileDependencies="10" TotalFileDependencies="50" AllowPageLevelTrace="false">

nebo ještě lepší způsob je mofikace systémovými prostředky Sharepointu, tedy prostředníctvím objektu SPWebConfigModification:

SPWebApplication webApp = siteCollection.WebApplication;
SPWebConfigModification callStackModification = 
     new SPWebConfigModification("CallStack", "configuration/SharePoint/SafeMode");
callStackModification.Value = "true";
callStackModification.Owner = typeof(Program).FullName;
callStackModification.Sequence = 0;
callStackModification.Type = 
     SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute;
SPWebConfigModification customErrorsModification = 
     new SPWebConfigModification("mode", "configuration/system.web/customErrors");
customErrorsModification.Value = "Off";
customErrorsModification.Owner = typeof(Program).FullName;
customErrorsModification.Sequence = 1;
customErrorsModification.Type = 
     SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute;
SPWebConfigModification debugModification = 
     new SPWebConfigModification("debug", "configuration/system.web/compilation");
debugModification.Value = "true";
debugModification.Owner = typeof(Program).FullName;
debugModification.Sequence = 2;
debugModification.Type = 
     SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute;
webApp.WebConfigModifications.Add(callStackModification);
webApp.WebConfigModifications.Add(customErrorsModification);
webApp.WebConfigModifications.Add(debugModification);
webApp.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
webApp.Update();

 

.NET COM Hell

Možná jste se již také řešili problém s volání COM objektů z .NET aplikace, wizardy a tutorialy vypadají moc pěkně a developer friendly, import COM objektů do referencí projektu klikací metodou je tak snadný, ale brzy každý narazí na problémy spojené se změnou rozhraní COM objektu, novou verzí atd. Před spuštění aplikace/aktivace assembly se provádí kontrola všech referencí a podreferencí projektu, v případě COM interop assembly se provádí kontrola shodné verze COM objektu pro kterou byl wrapper vytvořen – v tom je ale právě ten problém, pokud totiž voláte pouze jednu funkci, která se nemění, s další verzí COM objektu se celá aplikace která danou knihovnu používá nespustí (takže není možné například využívat COM objekty Office pro různé verze), ukončení aplikace není rovněž bezproblémové, protože se nedá nijak snadno zničit všechny reference na COM objekt i když voláte všude možně Marshal.ReleaseComObject (to je konkrétní případ pro Excelu)
Je tedy několik možností jak tento problém řešit:

[more]

  1. pro každou verzi vydávat service pack s novou verzí aplikace 🙁
  2. dynamicky generovat interop assembly a pak jí late bound užíva
  3. využít class RealProxy – Příklad užití je například v projektu SafeCOMWrapper

RealProxy
RealProxy je transparentní proxy kterou je možné řídit volání vzdálených objektů, překládá tak například volání funkcí klienta na specifické volání pro server (v tomto případě COM).

Základní předpokladem je znalost rozhraní serveru, pokud je DCOM server napsán v .NETu, je možné rozhraní vyextrahovat pomocí Reflectoru, v případě že je v C++ nebo jiném nativním jazyce, můžeme nejprve vytvořit interop assembly pomocí referencí ve Visual Studiu nebo příkazem tlbimp.exe a následně inteface přečíst pomocí Reflectoru.
Dostaneme tedy například:

public interface ICallMeCOM : IDisposable
{
    string HelloWorld(string sayThis);
    string HelloMessage { get; set; }
}

Teď zpět k projektu SafeCOMWrapper:
jedná se u ukázkovou implementaci možného využití RealProxy, v tomto referenčním projektu je navíc řešeno několik bugů jako například předávaní typu decimal referencí.
Kompletní tutorial najdete zde:
http://www.codeproject.com/csharp/SafeCOMWrapper.asp?df=100&amp;amp;forumid=195452&exp=0&select=1418263

Pokud do stejného projektu vložíte zdrojové soubory ze zmíněného odkazu, máte skoro vyhráno, nyní stačí jen objekt aktivovat:

ICallMeCOM callMe = (ICallMeCOM)COMWrapper.CreateInstance(Type.GetTypeFromProgID("PROGID-OBJEKTU"), typeof (ICallMeCOM));

Console.Write(callMe.HelloWorld("test"))

To je vše, jednoduché a spolehlivé 🙂