Archive for the 'SharePoint 2007' Category

System.Reflection.Konfusion

Entwickler sind faul – ich zumindest. Ich will möglichst wenig Code schreiben, aber strukturiert. Nun ergab es sich, das man einen Datentyp mit circa 80 eigenen Properties – welche ebenfalls wieder eigene Klassen sind – erstellen sollte. Dieser Job ist ja nun leicht erledigt. Man hacke fix ein Consolenprogramm welches aus einer XMl-Datei (in der die Klassen und Datentypen enthalten sind) einen Datentyp bastelt.
Das sieht dann letztendlich so aus (exemplarisch mal eine Klasse und der Datentyp):

public class ProductReference
{
        public string StaticName { get { return "ProductReference"; } }
        public string DisplayName { get { return Resources.Displayname_ProductReference; } }
        public Guid ID { get { return new Guid("{6ebe0f28-af6a-40f7-9ad8-d9ff37d2b045}"); } }
        public string TypeName { get { return "String"; } }
        public Type Type { get { return typeof(String); } }
        public String Value { get; set; }
	public bool ErrorsDetected { get; set; }
}
public class Item
{
	public ProductReference ProductReference { get; set; }
}

Jetzt soll dieser Datentyp natürlich auch irgendwo gespeichert werden. In meinem Fall ist es einfach eine SharePoint Liste bei der jedes Feld einer Klasseneigenschaft entspricht. (Das Speichern reicht natürlich nicht, es muss ja auch irgendwann wieder geladen werden etc.). Das Problem ist jetzt, das keiner Ernsthaft ~80 Properties einzeln in das ListItem der SharePoint Liste zu schreiben… geht natürlich auch.
Viel schöner ist aber, wenn wir einfach alle Properties der Klasse durchlaufen könnten und den Wert des entsprechenden Feldes zu setzen:

foreach (PropertyInfo mainprop in this.GetType().GetProperties())
{
	object fieldvalue = mainprop.GetValue(this, null).GetType().InvokeMember(
        	"Value", BindingFlags.GetProperty, null, mainprop.GetValue(this, null), null);
        string fieldname = (string)mainprop.GetValue(this, null).GetType().InvokeMember(
                    "DisplayName", BindingFlags.GetProperty, null, mainprop.GetValue(this, null), null);
        Guid fieldid = (Guid)mainprop.GetValue(this, null).GetType().InvokeMember(
                    "ID", BindingFlags.GetProperty, null, mainprop.GetValue(this, null), null);

        if (item.Fields.GetField(fieldname).Type == SPFieldType.DateTime)
                    fieldvalue = SPUtility.CreateISO8601DateTimeFromSystemDateTime((DateTime)fieldvalue);

        item[fieldid] = fieldvalue;

        try
        {
		item.SystemUpdate();
	}
	catch (Exception ex)
        {
		LogMsg(ex.ToString());
                ErrorsOccured = true;
                mainprop.GetValue(this, null).GetType().InvokeMember("ErrorsDetected", BindingFlags.SetProperty, null,
                mainprop.GetValue(this, null),
                new object[] { true });
        }
}

Dank Reflection können wir mittels this.GetType().GetProperties() über alle Properties der aktuellen Klasse iterieren. Jetzt wollen wir aber nicht den Wert der Property haben, sondern den Wert der Value Eigenschaft der Property. Klar? Schön. :)
Da man eine Site Columns im SharePoint am sichersten über seine Guid ansprechen kann, wird die Guid sowie der aktuelle Wert der Value Eigenschaft ausgelesen und in das SPListItem geschrieben. Kling konfus, ist es auch. ;) Mittels InvokeMember und der Option BindingFlags.GetProperty kann man grob gesagt den Wert auslesen. Analog kann man mittems BindingFlags.SetProperty den Setter des Members aufrufen.
Klingt alles irgendwie merkwürdig, und wir lesen ja meistens lieber Code – daher hier einfach mal das was dabei rausgekommen ist:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
using System.Collections;
using System.Reflection;
using Microsoft.SharePoint.Utilities;

namespace DataTypeTest
{
    #region class implementations
	// hier stehen alle Klassendefinitionen
	public class ProductReference
	{
	        public string StaticName { get { return "ProductReference"; } }
	        public string DisplayName { get { return Resources.Displayname_ProductReference; } }
	        public Guid ID { get { return new Guid("{6ebe0f28-af6a-40f7-9ad8-d9ff37d2b045}"); } }
	        public string TypeName { get { return "String"; } }
	        public Type Type { get { return typeof(String); } }
	        public String Value { get; set; }
		public bool ErrorsDetected { get; set; }
	}
	// etc.pp.
    #endregion

    public class NPPItem
    {
        #region properties
		// hier stehen die Properties
		public ProductReference ProductReference { get; set; }
		// etc. pp.
        #endregion

        public Action<string> Log;

        /// <summary>
        /// Initializes a new instance of the <see cref="Item"/> class.
        /// </summary>
        public Item(Action<string> LogFunction)
        {
            Log = LogFunction;
            InitFields();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Item"/> class.
        /// </summary>
        /// <param name="ListItemID">The list item ID.</param>
        /// <param name="WebUrl">The web URL.</param>
        /// <param name="ListId">The list id.</param>
        public Item(string ListItemID, string WebUrl, string ListId, Action<string> LogFunction, out bool ErrorsDetected)
        {
            if (string.IsNullOrEmpty(ListItemID) |
                string.IsNullOrEmpty(WebUrl) |
                string.IsNullOrEmpty(ListId))
                throw new ArgumentException("Empty params for Item(string ListItemID, string WebUrl, string ListId)!");
            Log = LogFunction;
            bool ErrorsOccured = false;
            int iItemId;
            if (!Int32.TryParse(ListItemID, out iItemId)) throw new ArgumentException("ListItemID is not a valid int!");

            InitFields();
            SPList liste = GetList(ListId, WebUrl);
            SPListItem litem = liste.GetItemById(iItemId);
            if (litem == null) { ErrorsDetected = true; return; }

            foreach (PropertyInfo mainprop in this.GetType().GetProperties())
            {
                Guid fieldid = (Guid)mainprop.GetValue(this, null).GetType().InvokeMember(
                    "ID", BindingFlags.GetProperty, null, mainprop.GetValue(this, null), null);

                SPField spfield = litem.Fields[fieldid];
                object[] param = new object[1];
                try
                {
                    param[0] = Convert.ChangeType(spfield.GetFieldValueAsText(litem[fieldid]),
                        mainprop.GetValue(this, null).GetType().GetProperty("Value").PropertyType);
                    mainprop.GetValue(this, null).GetType().InvokeMember("Value", BindingFlags.SetProperty, null,
                    mainprop.GetValue(this, null),
                    param);
                }
                catch (Exception ex)
                {
                    LogMsg(ex.ToString());
                    ErrorsOccured = true;
                    mainprop.GetValue(this, null).GetType().InvokeMember("ErrorsDetected", BindingFlags.SetProperty, null,
                    mainprop.GetValue(this, null),
                    new object[] { true });
                }

            }
            ErrorsDetected = ErrorsOccured;
        }

        private void InitFields()
        {
            // instanzen erstellen... sicher ist sicher :)
            this.ProductReference = new ProductReference();
	    // etc.pp.
        }

        /// <summary>
        /// Saves the item.
        /// </summary>
        /// <param name="ListItemID">The list item ID. Submit -1 to create a new item</param>
        /// <param name="Data">The data.</param>
	/// <returns>ID of the new list item</returns>
        public bool SaveItem(int ListItemID, string WebUrl, string ListId, out int newListItemID)
        {
            bool ErrorsOccured = false;
            SPList liste = GetList(ListId, WebUrl);
            if (liste == null)
                throw new ArgumentException(string.Format("List {0} in site {1} does not exist or you have insufficient permissions",
                    ListId, WebUrl));

            SPListItem item = null;
            if (ListItemID == -1) item = liste.Items.Add();
            else item = liste.Items[ListItemID];
            if (item == null) throw new ArgumentException(string.Format("ListItem with ID '{0}' does not exist in list '{1}' at site '{2}' !",
                ListItemID, ListId, WebUrl));

            foreach (PropertyInfo mainprop in this.GetType().GetProperties())
            {
                object fieldvalue = mainprop.GetValue(this, null).GetType().InvokeMember(
                    "Value", BindingFlags.GetProperty, null, mainprop.GetValue(this, null), null);
                string fieldname = (string)mainprop.GetValue(this, null).GetType().InvokeMember(
                    "DisplayName", BindingFlags.GetProperty, null, mainprop.GetValue(this, null), null);
                Guid fieldid = (Guid)mainprop.GetValue(this, null).GetType().InvokeMember(
                    "ID", BindingFlags.GetProperty, null, mainprop.GetValue(this, null), null);

                if (item.Fields.GetField(fieldname).Type == SPFieldType.DateTime)
                    fieldvalue = SPUtility.CreateISO8601DateTimeFromSystemDateTime((DateTime)fieldvalue);

                item[fieldid] = fieldvalue;

                try
                {
                    item.SystemUpdate();
                }
                catch (Exception ex)
                {
                    LogMsg(ex.ToString());
                    ErrorsOccured = true;
                    mainprop.GetValue(this, null).GetType().InvokeMember("ErrorsDetected", BindingFlags.SetProperty, null,
                    mainprop.GetValue(this, null),
                    new object[] { true });
                }
            }
            newListItemID = item.ID;
            return ErrorsOccured;
        }

        public List<string> GetPropertiesWithErrors()
        {
            List<string> result = new List<string>();
            foreach (PropertyInfo mainprop in this.GetType().GetProperties())
            {
                if (Convert.ToBoolean(mainprop.GetValue(this, null).GetType().InvokeMember(
                    "ErrorsDetected", BindingFlags.GetProperty, null, mainprop.GetValue(this, null), null)))
                    result.Add(mainprop.Name);
            }
            return result;
        }

        /// <summary>
        /// Gets a SPList.
        /// </summary>
        /// <param name="ListId">The list id.</param>
        /// <param name="SiteUrl">The site URL.</param>
        /// <returns>SPList if found, otherwise null</returns>
        private SPList GetList(string ListId, string SiteUrl)
        {
            if (ToGuid(ListId) == null) return null;
            SPList retval = null;
            using (SPSite site = new SPSite(SiteUrl))
            {
                using (SPWeb web = site.OpenWeb())
                {
                    retval = web.Lists[ToGuid(ListId)];
                }
            }
            return retval;
        }

        /// <summary>
        /// Get a Guid object from a valid guid string
        /// </summary>
        /// <param name="input">The input.</param>
        /// <returns>valid GUID or null</returns>
        private Guid ToGuid(string input)
        {
            if (string.IsNullOrEmpty(input)) throw new ArgumentException("input is null");
            Guid retval = new Guid();
            try
            {
                retval = new Guid(input);
            }
            catch (Exception) { }
            return retval;
        }

        private void LogMsg(string Message)
        {
            if (this.Log != null)
                Log(Message);
        }

    }
}

Nice to have wäre natürlich noch, die ganze Show mit Interfaces zu versehen, so dass man nicht mehr hardcoded auf die Subpropertynamen losgehen muss, wie ich es bisher gemacht habe:

mainprop.GetValue(this, null).GetType().InvokeMember("Value", BindingFlags.GetProperty, null, mainprop.GetValue(this, null), null);

Ich brauch jetzt erstmal ein wenig Frischluft. :)

To yield or not to yield

Das yield Keyword ist ein in C# häufig unterschätztes Keyword. Die MSDN – bzw. die C# Spezifikation – sagt dazu:

Def: Wird in einem Iterator-Block verwendet, um für das Enumerationsobjekt einen Wert zu liefern oder um das Ende der Iteration zu signalisieren.
Der Ausdruck wird ausgewertet und als ein Wert an das Enumerationsobjekt zurückgegeben. Der expression muss implizit in den yield-Typ des Iterators konvertierbar sein.
Die yield-Anweisung kann nur innerhalb eines iterator-Blocks stehen, der als Rumpf einer Methode, eines Operators oder eines Accessors verwendet wird. Der Rumpf solcher Methoden, Operatoren oder Accessoren unterliegt folgenden Einschränkungen:

- Unsichere Blöcke werden nicht zugelassen.
- Parameter für die Methode, den Operator oder den Accessor dürfen nicht ref oder out sein.

Eine yield-Anweisung darf nicht in einer anonymen Methode stehen. Weitere Informationen finden Sie unter Anonyme Methoden (C#-Programmierhandbuch).
Bei Verwendung mit expression darf eine yield return-Anweisung nicht in einem catch-Block oder einem try-Block stehen, der mehr als eine catch-Klausel enthält.

Wie sieht das jetzt in der Praxis aus?
Das Beispiel in der MSDN ist jetzt nicht besonders praxistauglich, daher hier mal eine andere Verwendungsweise:

class Program
    {
        static void Main(string[] args)
        {
            string searchedCRMId = "de7ea510-8e55-de11-a210-0017084f5232";
            Console.WriteLine("Starting to search for user with CRM_ID '{0}'...", searchedCRMId);
            foreach (UserProfile item in GetProfiles("http://your_ssp_host/"))
            {
                string currentID = Convert.ToString(item["CRMUserID"].Value);
                if (string.IsNullOrEmpty(currentID))
                    continue;

if (currentID.Equals(searchedCRMId, StringComparison.CurrentCultureIgnoreCase))
                {
                    Console.WriteLine("Found searched user!\nAccountname: {0}", item[PropertyConstants.AccountName].Value);
                    break;
                }
            }
            Console.WriteLine("Done");
            Console.ReadLine();
        }

static IEnumerable<UserProfile> GetProfiles(string SharedServiceProviderUrl)
        {
            SPWebApplication webapp = SPWebApplication.Lookup(new Uri(SharedServiceProviderUrl));
            UserProfileManager upm = new UserProfileManager(ServerContext.GetContext(webapp));
            foreach (UserProfile profile in upm)
            {
                yield return profile;
            }
        }
    }

Diese ConsoleApplication iteriert über alle vorhandenen UserProfile und vergleicht eine CustomProperty mit einem Suchwert. In der GetProfiles() Methode wird allerdings mittels “yield return profile” das aktuelle Profil aus der Auflistung zurückgegeben und die übergeordnete foreach-Schleife weiter bearbeitet. Das spart zum einen Performance und zum anderen Speicher.
Zu beachten ist dabei allerdings, das bei folgender Verwendung immer das erste Item “verschwindet”:

IEnumerable<UserProfile> profileList = GetProfiles("http://your_ssp_host/");
foreach (UserProfile item in profileList)
{
 string currentID = Convert.ToString(item["CRMUserID"].Value);
        if (string.IsNullOrEmpty(currentID))
         continue;

if (currentID.Equals(searchedCRMId, StringComparison.CurrentCultureIgnoreCase))
        {
         Console.WriteLine("Found searched user!\nAccountname: {0}", item[PropertyConstants.AccountName].Value);
                break;
        }
}

Die Ursache ist recht einfach:

Typische Implementation Implementation mit yield
1. Funktionsaufruf 1.Funktionsaufruf
2.Funktion wird ausgeführt und gibt eine Liste zurück 2.Aufrufer fordert ein Item an
3.Aufrufer arbeitet mit der Liste 3.Nächstes Item aus der Auflistung wird zurückgegeben
  4.Sprung zu 2. solange wie der Aufrufer kein Item mehr haben will (break im Caller) oder keine Items mehr vorhanden sind

Was man letztendlich verwendet ist natürlich immer von der Anforderung abhängig. :)

The following error was encountered while reading module ‘Microsoft.SharePoint’: Could not resolve type: ObjectModel.

Bei Projekten mit einer Referenz auf die “Microsoft.SharePoint.dll” laufen keine Code Metric Analysen. Der Grund ist nach einer kurzen Google-Recherche auch schnell gefunden: Ein Bug im Visual Studio 2008.
Ein Workaround gibts auch dazu: Man muss lediglich eine Referenz auf die “C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\ISAPI\Microsoft.SharePoint.Security.dll” einbinden, dann klappts auch mit der Analyse.

SharePoint: Alle Sharedservice Provider ermitteln

Ab und an kommt mal wieder die Anforderung einen Timerjob an die Webapplication des Sharedserviceproviders anzuhängen. Bei einem SSP ist das auf den ersten Blick auch kein Problem: Entweder die URL als Konfigurationseinstellung verdraten oder über eine Settingspage in der Zentraladministration konfigurierbar machen. Spätestens wenn der Kunde aber mehr als einen SSP im Einsatz hat, und der Timerjob an alle SSPs angehängt werden soll, steht man vor einem Problem: Entweder immer wieder Einstellungen hinzufügen oder man überlegt sich eine generelle Lösung.

Jeder SSP verfügt über eine Administrationsoberfläche die mit einer bestimmten Sitedefinition erzeugt wurde. Diese findet man im 12er Hive unter TEMPLATE\SiteTemplates\OSRV. Wie kann man sich nun eine Liste aller vorhandenen SSPs erzeugen? Zum Beispiel mit folgender Funktion:

        public static List GetSharedServiceProviders()
        {
            List result = new List();
            SPFarm farm = SPFarm.Local;
            SPWebService service =
                farm.Services.GetValue();
            foreach (SPWebApplication webApp in
                service.WebApplications)
            {
                if (webApp.Sites.Count == 0)
                    continue;
                using (SPSite rootsite = webApp.Sites[0])
                {
                    if (rootsite.AllWebs.Count == 0)
                        continue;
                    using (SPWeb rootweb = rootsite.RootWeb)
                    {
                        if (rootweb.WebTemplateId == 40)
                            result.Add(rootsite.Url);
                    }
                }
            }
            return result;
        }

Eigentlich ist es simpel: Man holt sich aus der Farm alle WebApplications, öffnet die Rootsite und prüft mit welchem SiteTemplate das Rootweb angelegt wurde. Ist die TemplateID gleich der TemplateID der SSP Sitedefinition, dann ist es ein Sharedservice Provider.