Social Icons

Mostrando entradas con la etiqueta SharePoint 2010. Mostrar todas las entradas
Mostrando entradas con la etiqueta SharePoint 2010. Mostrar todas las entradas

miércoles, 11 de febrero de 2015

Se acabaron las funciones recursivas para definir CAML Queries gracias a Camlex

Algunas veces tienes un número aleatorio de condiciones para incluir en una CAML query y en esos casos yo solía definir las consultas usando una función recursiva que normalmente tenía que debugar unas cuantas veces me funciona siempre a la primera.

El código para este tipo de consultas sería algo así (y este es de los simples):

public List<string> GetSomeInfo(string fieldsToSearch, string contentTypesToSearch)
{
    ...

    var queryval = string.Empty;
    if (contentTypesToSearch.IsNullOrEmpty())
        queryval = string.Format("<Where>" + GenerateFieldsQuery(fieldsToSearch.Split(','), 0) + "</Where>", text);
    else
        queryval = string.Format("<Where><And>" + GenerateCTypesQuery(contentTypesToSearch.Split(','), 0) + GenerateFieldsQuery(fieldsToSearch.Split(','), 0) + "</And></Where>", text);

    var scope = "Scope=\"RecursiveAll\"";

    ...
}

private static string GenerateFieldsQuery(string[] fields, int index)
{
    if (fields.Length == 0) return string.Empty;

    if (fields.Length == index + 1)
        return "<Contains><FieldRef Name='" + fields[index] + "' /><Value Type='Text'>{0}</Value></Contains>";

    return "<Or><Contains><FieldRef Name='" + fields[index] + "' /><Value Type='Text'>{0}</Value></Contains>" + GenerateFieldsQuery(fields, ++index) + "</Or>";
}

private static string GenerateCTypesQuery(string[] cTypes, int index)
{
    if (cTypes.Length == 0) return string.Empty;

    if (cTypes.Length == index + 1)
        return "<Eq><FieldRef Name='ContentType' /><Value Type='Choice'>" + cTypes[index] + "</Value></Eq>";

    return "<Or><Eq><FieldRef Name='ContentType' /><Value Type='Choice'>" + cTypes[index] + "</Value></Eq>" + GenerateCTypesQuery(cTypes, ++index) + "</Or>";
}

Eso era hasta ahora... gracias a Camlex (y gracias a Luis por decírmelo), este código se puede escribir así:

public List<string> GetSomeInfo(string fieldsToSearch, string contentTypesToSearch)
{
    ...

    var queryVal = string.Empty;
    var fieldExtensions = new List<Expression<Func<SPListItem, bool>>>();
    var cTypeExtensions = new List<Expression<Func<SPListItem, bool>>>();

    if (!contentTypesToSearch.IsNullOrEmpty())
    {
        foreach (var cType in contentTypesToSearch.Split(','))
            cTypeExtensions.Add(x => (string)x["ContentType"] == cType);
    }

    foreach (var field in fieldsToSearch.Split(','))
        fieldExtensions.Add(x => ((string)x[field]).Contains(text));

    var expressions = new List<Expression<Func<SPListItem, bool>>>();
    expressions.Add(ExpressionsHelper.CombineOr(cTypeExtensions));
    expressions.Add(ExpressionsHelper.CombineOr(fieldExtensions));

    queryVal = Camlex.Query().WhereAll(expressions).ToString();

    ...
}

Echaré de menos los métodos recursivos... me hacían sentir especial...

No hay comentarios:

jueves, 19 de septiembre de 2013

¿Qué campos hay disponibles en un SPListItem?

Cuando traes los elementos usando una consulta CAML con el parámetro ViewFields configurado no hay manera (o al menos yo no la conozco) de encontrar qué campos tienes disponibles y con datos.

La forma estandar de coger la lista de campos del tipo de contenido no vale porque el ContentType es null en esta clase de elementos…

La forma más fácil para ver qué tenemos es mirar en el XML del elemento.

static List<string> ExcludedFields = new List<string> { "z", "ows_ServerRedirected", "ows_FileRef", "ows_PermMask", "ows_FSObjType", "ows__Level", "ows__ModerationStatus" };
/// <summary>
/// Gets a dictionary with the available fields and its values.
/// </summary>
public static Dictionary<string, string> getAvailableFields(this SPListItem item)
{
    XElement row = XElement.Parse(item.Xml);
    Dictionary<string, string> Fields = new Dictionary<string, string>();

    foreach (XAttribute field in row.Attributes())
    {
        if (!ExcludedFields.Contains(field.Name.LocalName))
            Fields.Add(field.Name.LocalName.Substring(4), field.Value);
    }

    return Fields;
}

Usando esto he logrado reducir el tráfico que generaba un método de un web service en un 65%.

No hay comentarios:

¿Cuánto estás trayendote desde SharePoint en tus consultas CAML?

Yo no se tú, pero yo me estoy trayendo más de la cuenta.

Esta es una de esas cosas que no notas hasta que es demasiado tarde. Yo creía que sólo con añadir filtros estrictos a mis consultas CAML para traer estrictamente los elementos que necesitaba era suficiente pero hay algo más que puedes hacer.

Puedes restringir los campos que te estás trayendo... y debes.

La consulta SQL que SharePoint genera para traerse los elementos cuando dejas el parámetro ViewFields de la CAML vacío casi duplica la complejidad de la consulta que se genera cuando especificas los campos que quieres usar o usas una vista.

Dos simples líneas como estas:

query.ViewFields = "<FieldRef Name=\"Value\" />";
query.ViewFieldsOnly = true;
Pueden hacer tu query mejor.

¿Quién podría resistirse cuando es así de simple?

No hay comentarios:

viernes, 23 de agosto de 2013

Scopes en una consulta CAML

He estado trabajando bastante tiempo con CAML queries y el scope, también conocido como ámbito, es siempre algo muy importante a tener en cuenta. ¿Cuántas veces mis consultas no devuelven nada cuando estoy seguro de que deberían tener resultados…

Básicamente tenemos dos modificadores Recursive y All y nada, ¿Podemos llamar a "nada" un modificador? All va a devolver ficheros y carpetas. Recursive repetirá la query en todas las carpetas bajo la que hemos establecido como raíz para la consulta.

Si no pones el scope a All solo traerás ficheros. Si no lo pones a Recursive solo traeras elementos de la carpeta en la que estás trabajando. No hay tantas posibilidades por lo que voy a hacer un ejemplo de cada una.

Vamos a imaginar que tenemos una carpeta de SharePoint como esta y que vamos a lanzar consultas contra ella:

CamlScopeTreeSample

Ya se que no soy bueno con el paint pero lo que quiero mostrar aquí es un arbol en donde tenemos una carpeta que será la raíz de las queries que lancemos(Root), dos sub carpetas y algunos ficheros. Para cada posible caso resaltaré los elementos que puedes esperar recibir de la query.

Antes de empezar te recuerdo que el scope se selecciona en la propiedad ViewAttributes del objeto SPQuery.

Para que quede todavía más claro he pintado los niveles:

CamlScopeTreeSampleLevels

La línea verde mara lo que está dentro de la carpeta raíz, la azul lo que esta dentro de SubFolder1 y la roja el contenido de SubFolder2.

ViewAttributes dejado por defecto:

CamlScopeByDefault
Esto traerá solo los ficheros de la carpeta raíz.

ViewAttributes = "Scope='Recursive'"

CamlScopeRecursive

Esto traerá todos los ficheros de todas las carpetas.

ViewAttributes = "Scope='All'"

CamlScopeAll

Esto devolverá ficheros y carpetas bajo la raíz.

ViewAttributes = "Scope='RecursiveAll'"


CamlScopeRecursiveAll

Y finalmente con RecursiveAll puedes traerte todo lo que hay dentro de la carpeta raíz.

Buena suerte con las consultas.

No hay comentarios:

lunes, 19 de agosto de 2013

El modelo de objetos de cliente es fantástico

Esta es una de esas cosas que sabes que están ahí pero que nunca usas porque ya sabes cuarenta maneras diferentes de hacerlo.

Tan solo cuatro años después de haber empezado a trabajar con SharePoint 2010 pensé "¿Por qué no darle una oportunidad al Modelo de Objetos de Cliente de SharePoint?" en realidad lo que pasó es que tuve un requerimiento de un cliente y debo decir que estoy MUY impresionado. La simplicidad, la velocidad y la predecibilidad son buenísimas, para los estádares a los que estamos acostumbrados.

Aunque el MOC (o COM) es muy bueno me he creado unos cuantos metodos y extensiones que me ayudan a manejar las situaciones más comunes. Algunas son las mismas que uso en el modelo de objetos de SharePoint pero traducidas a COM y otras son nuevas, vamos a empezar:

Es muy fácil conectar usando autenticación windows o basada en formularios. Más sencillo que ninguna otra cosa que haya visto hasta la fecha.

public static ClientContext GenerateClientContextWinAuth(string URL)
{
    return new ClientContext(URL);
}

public static ClientContext GenerateClientContextFBAAuth(string URL, string userName, string password)
{
    ClientContext ctx = new ClientContext(URL);
    ctx.AuthenticationMode = ClientAuthenticationMode.FormsAuthentication;
    ctx.FormsAuthenticationLoginInfo = new FormsAuthenticationLoginInfo(userName, password);

    return ctx;
}


Windows auth 1 linea de código FBA 3. Bien.

¿Te acuerdas de como era leer y escribir ficheros de SharePoint? Pue mira qué fácil es con el modelo de objetos de cliente.

public static string ReadFile(ClientContext Context, string ListName, string FileName)
{
    List StorageList = Context.Web.Lists.GetByTitle(ListName);
    string SharePointFilePath = GetFilePathInSharePoint(Context, StorageList, FileName);

    FileInformation fileInfo = Microsoft.SharePoint.Client.File.OpenBinaryDirect(Context, SharePointFilePath);

    using (fileInfo.Stream)
    {
        using (StreamReader sr = new StreamReader(fileInfo.Stream))
        {
            return sr.ReadToEnd();
        }
    }
}

public static void WriteFile(ClientContext Context, string LocalFilePath, string ListName, string SPFileName)
{
    List StorageList = Context.Web.Lists.GetByTitle(ListName);

    string SharePointFilePath = GetFilePathInSharePoint(Context, StorageList, SPFileName);

    using (FileStream fs = new FileStream(LocalFilePath, FileMode.Open))
        Microsoft.SharePoint.Client.File.SaveBinaryDirect(Context, SharePointFilePath, fs, true);
}

private static string GetFilePathInSharePoint(ClientContext ctx, List StorageList, string FileName)
{
    if (StorageList.RootFolder.ServerObjectIsNull != false)
    {
        ctx.Load(StorageList.RootFolder);
        ctx.ExecuteQuery();
    }

    string ListRootFolderURLDocuments = StorageList.RootFolder.ServerRelativeUrl;
    return Path.Combine(ListRootFolderURLDocuments, FileName);
}


Quitando la funcioncita para encontrar la url del elemento en la biblioteca de documentos más fácil imposible.

También me he creado un par de métodos para facilitar el trabajo con el StratexFramework.

Si estás desarrollando un programa y quieres usar algo de este código perfecto y si no pues puedes modificarlo para que se adecúe a tu entorno. Aquí hay un método para traerse la entidad raíz del framework:

public static ListItem GetRootEntity(this List ComList)
{
    CamlQuery camlQuery = new CamlQuery();
    camlQuery.ViewXml = string.Format(@"<View>
                                            <Query>
                                                <Where>
                                                    <Eq>
                                                        <FieldRef Name='ContentType'/>
                                                        <Value Type='Choice'>Entity</Value>
                                                    </Eq>
                                                </Where>
                                                <RowLimit>1</RowLimit>
                                            </Query>
                                        </View>");

    ListItemCollection items = ComList.GetItemsExecuted(camlQuery);

    if (items.Count == 1)
        return items[0];
    else
        return null;
}

Bonito y fácil.

La query es un pelín distinta de la que usamos siempre en el SPQuery pero aún así bastante similar.

Después me he creado un método que te ayudará cuando quieras hacer una query dentro de una carpeta sin importarte el resto del framework. Primero voy a poner el helper y luego la aplicación para que veáis como lo utilizo:

public static CamlQuery CreatePositionedQuery(this ListItem StartEntity)
{
    CamlQuery camlQuery = new CamlQuery();
    camlQuery.ViewXml = string.Format(@"<View Scope='RecursiveAll' >
                                            <Query>
                                                <Where>
                                                    <And>
                                                        <Eq><FieldRef Name='FileDirRef' /><Value Type='Text'>{0}</Value></Eq>
                                                        {1}
                                                    </And>
                                                </Where>
                                            </Query>
                                        </View>", GetChildrenFolder(StartEntity), "{0}"); //This is the folder url and the placeholder for the real query.


    return camlQuery;

}

private static string GetChildrenFolder(ListItem startItem)
{
    startItem.InitializeIfNeeded("FileDirRef");
    startItem.InitializeIfNeeded("FileLeafRef");

    return startItem["FileDirRef"] + "/" + startItem["FileLeafRef"];
}

Con esto, básicamente le decimos a SharePoint que ejecute la query debajo de la carpeta del elemento que le estamos pasando como parámetro. Y de esta manera podemos posicionar fácilmente nuestras consultas en el arbol de carpetas.

Antes que nada, una función para traer todas las entidades hijas que están debajo de una entidad dada y luego otra función para traer un elemento con un nombre dado bajo una carpeta dada:

public static ListItemCollection GetChildEntities(ListItem StartEntity)
{
    List ComList = StartEntity.ParentList;

    CamlQuery camlQuery = CreatePositionedQuery(StartEntity);
    camlQuery.ViewXml = string.Format(camlQuery.ViewXml,
                                    @"<And>
                                        <Eq><FieldRef Name='State' /><Value Type='Choice'>Live</Value></Eq>
                                        <Eq><FieldRef Name='ContentType' /><Value Type='Choice'>Entity</Value></Eq>
                                        </And>");

    return ComList.GetItemsExecuted(camlQuery);
}

public static ListItem GetChildItem(this ListItem StartEntity, string Title)
{
    List ComList = StartEntity.ParentList;

    CamlQuery camlQuery = CreatePositionedQuery(StartEntity);
    camlQuery.ViewXml = string.Format(camlQuery.ViewXml,
                                    string.Format("<Eq><FieldRef Name='Title' /><Value Type='Text'>{0}</Value></Eq>", Title));

    ListItemCollection result = ComList.GetItemsExecuted(camlQuery);

    if (result.Count > 0)
        return result[0];
    else
        return null;
}
Crear un elemento nuevo e también muy fácil. Lo más complicado es decidir si el elemento es carpeta u hoja:
public static ListItem CreateItemUnder(this ListItem ParentItem, string Title, string ContentType, bool isLeaf)
{
    ListItemCreationInformation itemCreateInfo = new ListItemCreationInformation();
    itemCreateInfo.FolderUrl = GetChildrenFolder(ParentItem);
    itemCreateInfo.LeafName = Title;

    if (isLeaf)
        itemCreateInfo.UnderlyingObjectType = FileSystemObjectType.File;
    else
        itemCreateInfo.UnderlyingObjectType = FileSystemObjectType.Folder;

    ListItem NewItem = ParentItem.ParentList.AddItem(itemCreateInfo);

    if (ContentType != null)
        NewItem["ContentTypeId"] = GetContentType(ParentItem.ParentList, ContentType).Id;

    return NewItem;
}
Un pequeño atajo para ejecutar consultas:
public static ListItemCollection GetItemsExecuted(this List listToQuery, CamlQuery query)
{
    ListItemCollection childItems = listToQuery.GetItems(query);

    listToQuery.Context.Load(listToQuery);
    listToQuery.Context.Load(childItems);
    listToQuery.Context.ExecuteQuery();

    return childItems;
}
En algunos casos el campo que quieres leer no viene en la ejecución de la query quizá porque hass sido una mijita más restrictivo de la cuenta con el parametro viewfields... Puedes probar este truco:
public static void InitializeIfNeeded(this ListItem item, string InternalName)
{
    if (!item.FieldValues.ContainsKey(InternalName))
    {
        item.Context.Load(item);
        item.Context.ExecuteQuery();
    }
}

¿Estás intentando conseguir un tipo de contenido de una lista? Fácil.


public static ContentType GetContentType(List list, string cTypeName)
{
    list.Context.Load(list.ContentTypes);
    list.Context.ExecuteQuery();

    foreach (ContentType ctype in list.ContentTypes)
    {
        if (ctype.Name == cTypeName) return ctype;
    }

    return null;
}

¿Quieres guardar un SPFieldUserValue en un SPFieldUser usando el modelo de objetos de cliente o necesitas leerlo desde allí? Complicado. Yo diría que innecesariamente complicado, pero tengo un último truco:

public static string UserToString(object fieldUserValue)
{
    if (fieldUserValue == null) return string.Empty;

    FieldUserValue user = fieldUserValue as FieldUserValue;

    return string.Format("{0};{1}", user.LookupId, user.LookupValue);
}

public static FieldUserValue StringToUser(object fieldUserValueString)
{
    FieldUserValue user = new FieldUserValue();

    if (string.IsNullOrEmpty(fieldUserValueString.ToStringSafe())) return user;

    string[] tokens = fieldUserValueString.ToStringSafe().Split(';');

    user.LookupId = tokens[0].ToNullableInt() ?? 0;

    return user;
}

El modelo de objetos de cliente me ha parecido potente y muy facil de usar. Te aconsejo que lo uses, si no lo haces ya.

No hay comentarios:

lunes, 22 de abril de 2013

Editando SPListItems desde SPWeb.GetSiteData

En mi solución tengo un número desconocido de subsitios que contienen un número desconocido de SPListItems que necesitan ser actualizados. Parece la manera perfecta de probar el  GetSiteData.

La documentación es útil y podrás probar las consultas sin problemas ¿De verdad? Síp la documentación está bien pero este método trae de vuelta un objeto DataTable y yo lo que necesito es actualizar los SPListItems.

Bueno, si miras al DataTable verás que tienes todo lo necesario para traerte el SPListItem fácilmente.

Me he creado un extension method, bueno dos,... Me encantan los extension methods.
public static SPListItem GetListItemFromSiteData(this DataRow ItemRow, SPSite ParentSite)
{
    using (SPWeb Web = ItemRow.GetWebSiteData(ParentSite))
    {
        return Web.Lists[new Guid(ItemRow["ListId"].ToString())].GetItemById(Convert.ToInt32(ItemRow["ID"]));
    }
}

public static SPWeb GetWebSiteData(this DataRow ItemRow, SPSite ParentSite)
{
    return ParentSite.OpenWeb(new Guid(ItemRow["WebId"].ToString()));
}

Usando estos dos podrás iterar fácilmente por la colección de filas del DataTable, seleccionar qué elementos necesitan ser actualizados y actualizarlos.

No he probado cómo de rápido es comparado con traerte los objetos con una CAML query. No tener que crear la SPListItemCollection podría hacer este método más rápido y más conveniente para según que cosas... Lo probaré,

No hay comentarios:

lunes, 8 de abril de 2013

Localización de Nombres de Listas en SharePoint

No se puede llamar a una lista por su nombre interno por defecto en SharePoint o por lo menos, yo no se cómo, tienes que usar el nombre externo o el GUID y eso es un problema cuando hay que localizar.

Para solucionar este problema he creado un par de extensiones basadas en la URL. La idea es simple, uso una serie de variables para almacenar el "nombre interno" de las listas y siempre las llamo usando estas constantes. Hasta ahora ha funcionado bien.
/// <summary>
/// Returns null if the list is not found
/// </summary>
public static SPList GetListByInternalName(this SPListCollection Lists, string InternalName)
{
    if (string.IsNullOrEmpty(InternalName)) return null;

    Guid ListGuid = Lists.GetListIdByInternalName(InternalName);

    if (ListGuid != Guid.Empty)
        return Lists[ListGuid];

    return null;
}

/// <summary>
/// Returns the UIDC of the list
/// </summary>
static Guid GetListIdByInternalName(this SPListCollection Lists, string InternalName)
{
    if (string.IsNullOrEmpty(InternalName)) return Guid.Empty;

    foreach (SPList list in Lists)
        if (list.GetInternalName().ToLower() == InternalName.ToLower())
            return list.ID;

    return Guid.Empty;
}

/// <summary>
/// Gets the Url of the list. That's what we consider its internal name
/// </summary>
public static string GetInternalName(this SPList list)
{
    string Url = list.RootFolder.ServerRelativeUrl;

    string[] split = Url.Split('/');

    return split[split.Length - 1];
}

Después de esto el nombre externo de la lista no es relevante y la localización es simple o, por lo menos, un poco más simple.

No hay comentarios:

domingo, 7 de abril de 2013

Los 5 errores más tontos (y comunes) de los desarrolladores de SharePoint

Si eres un desarrollador veterano probablemente estarás de acuerdo, si no tienes tanta experiencia probablemente has sufrido por lo menos uno de estos la semana pasada y perdiste una hora intentando enterarte de lo que estaba pasando.

Actualizando la web que no es
Estás cambiando algo, el código funciona, no se lanzan excepciones y vas al sitio de SharePoint para ver tu nueva obra pero no ha cambiado nada... Comprueba que estás cambiando la web que querías modificar y no otra.

Nombre de campo incorrecto
Tu CAML query esta bien, pero recibes una excepción diciendo que los campos de la lista están mal. no es la lista, eres tu Vete a los settings de la lista, a la definición del campo y alli, en la URL tienes el nombre interno del campo tal y como SharePoint espera recibirlo, si es verdad, lo tenías mal en la CAML. A mi me gusta tenerlos definidos como constantes en algún lugar, escríbelos bien una vez y deja que el intellisense te recuerde el nombre por el resto de la vida del proyecto.

Lista equivocada
El fichero que estas creando no aparece en la lista o se queja de los campos y ya lo has comprobado y están bien... Comprueba que estás conectad a la lista correcta. (Por cierto, también tengo constantes para los nombres de las listas)

DLL Incorrecta
Cambias algo en tu código pero el error sigue ahí. Dependiendo de lo que estes haciendo necesitas desplegar la DLL al gac o a la carpeta BIN, la DLL correcta, y luego resetear el servicio correcto, algunas veces no es el IIS. Intenta cambiar una string o algo muy obvio y fácil de notar para asegurarte de que estás ejecutando el código que quieres ejecutar.

Mis web services no se conectan al servidor
Necesitas crear un fichero clientaccesspolicy.xml o un crossdomain.xml y ponerlo en la carpeta de la aplicación web en el inetpub tan simple y tan molesto.
[BONUS] Los cambios que hago no aparecen en SharePoint
La lista está bien, la web está bien, los campos están bien, todo está bien y no pasa nada después de que haya cambiado el objeto... Se te ha olvidado hacer el update. Y esto es válido para un montón de objetos diferentes en SharePoint. En caso de duda yo siempre intento hacer un .Update después de modificar. Cuidado, los updates son normalmente lentos y deben ser llamados la menor cantidad de veces posible..

No hay comentarios:

viernes, 5 de abril de 2013

Encriptación de Datos Transparente para SharePoint

SharePoint implementa seguridad a nivel de usuario, si quieres acceder a un fichero pero no tienes los permisos necesarios serás bloqueado y eso es suficiente para la gran mayoría de las empresas, pero...

- ¿Qué pasa si alguien consigue acceder a tu servidor de bases de datos?
+ Podrá fácilmente descargarse la base de datos de contenido, conectarla a uno de sus servidores de SharePoint y tener acceso a todo hasta el branding.
- ¿Qué puedo yo hacer ante esto como desarrollador de SharePoint?
+ Nada.
- ¡Ay, Dios mio! ¡Ay, Dios mio!
+ Tranquilízate muchacho, si quieres encriptar las bases de datos pídeselo al tío de las bases de datos.

Sí, los administradores de base de datos pueden encriptar instancias de SQL usando TDE. Y, aunque parece fácil, estoy contento de no tener que hacerlo yo.

- ¡Pero encriptar todo va a afectar al rendimiento de mi granja!
+ Microsoft cree que solo será entre un 3 y un 5% más lenta.

Puedes conseguir más información sobre este tema aquí y aquí (por ejemplo).

No hay comentarios:

miércoles, 3 de abril de 2013

Compartiendo Datos Entre Front Ends con Thread Safe Web Properties

¿Te acuerdas en los viejos tiempos cuando solo tenías un frontend? ¿Te acuerdas cuando pensabes que era complicado?

Las cosas han cambiado y ahora para casi cualquier entorno de producción tienes unos cuantos frontents y está bien, el rendimiento mejora y, con SharePoint, es fácil meter uno más si te parece que vas corto pero también ha llevado algunas tareas a un nuevo nivel de complejidad.

Hay software por ahí como AppFabric que te permite persistir datos y compartirlos entre todos tus servidores. AppFabric es bueno pero... ¿Vas a instalar y configurar todo en todos los servers para compartir un string de 10 caracteres?

Bueno, no te hace falta. Puedes usar las Web Properties de SharePoint para hacerlo. Son rápidas de leer, de escribir, de programar y no tienes que configurar nada.

Las Web Properties de SharePoint son un poco raritas a la hora de usarlas, especialmente cuando las usas en un proceso multi hilo, pero estos wrappers pueden ahorraros algún tiempo. (Yo los he usado en un par de escenarios y funcionan, pero no puedo asegurar que sean completamente seguros)

static object PropertyWriteLock = new object();

/// <summary>
/// Sets a web property to a given value. This method is thread safe.
/// </summary>
/// <param name="Key">The Key for the web property (case insensitive)</param>
/// <param name="Value">Value to set the property to</param>
public static void SetWebProperty(this SPWeb Web, string Key, string Value)
{
    Key = Key.ToLower();
    lock (PropertyWriteLock) //It's better to have a lock just for the key we are working with.
    {
        if (GetWebPropertyThreadSafe(Web, Key) != Value)
        {
            Web.Properties[Key] = Value;

            Web.Properties.Update();
        }
    }
}

/// <summary>
/// Returns the web property with the given key. This method is thread safe.
/// </summary>
/// <param name="Key">The Key for the web property (case insensitive)</param>
/// <returns>Returns null if not found</returns>
public static string GetWebPropertyThreadSafe(this SPWeb Web, string Key)
{
    Key = Key.ToLower();
    using (SPSite site = new SPSite(Web.Site.ID))
    {
        using (SPWeb newWeb = site.OpenWeb(Web.ID))
        {
            return newWeb.GetWebProperty(Key);
        }
    }
}

/// <summary>
/// Returns the web property with the given key.
/// </summary>
/// <param name="Key">The Key for the web property (case insensitive)</param>
/// <returns>Returns null if not found</returns>
public static string GetWebProperty(this SPWeb Web, string Key)
{
    Key = Key.ToLower();

    return Web.Properties[Key];
}

/// <summary>
/// Removes the web property from the web
/// </summary>
/// <param name="Key">The Key for the web property (case insensitive)</param>
/// <remarks>The web property will remain there but set to null.</remarks>
public static void RemoveWebProperty(this SPWeb Web, string Key)
{
    if (Web.Properties.ContainsKey(Key))
        Web.Properties.Remove(Key);

    Web.Properties.Update();

    if (Web.AllProperties.ContainsKey(Key))
        Web.AllProperties.Remove(Key);

    Web.Update();
}

Esto ha simplificado algunas partes de la aplicación un montón y, voy a decirlo otra vez, es rápido.

Pruébalo y me cuentas.

No hay comentarios:

martes, 2 de abril de 2013

Iterando Listas in Trozos Pequeños y Rendimiento

Hace un par de meses publiqué un post sobre esto y a modo de conclusión dije que me parecía que sería más rápido recorrer la lista entera en trozos pequeños en lugar de hacerlo todo de golpe con una query general... pues no.

He hecho un test con una lista de 100K items para ahorrarte el trabajo y dependiendo en el tamaño de los trozos puede tardar un poco más o un montón más.

Si estás usando un RowLimit pequeño (digamos 5) el numero de veces que tienes que realizar la query para traerte la lista completa puede hacer que el proceso tarde el doble o más. Si estás usando un número mayor (digamos 1000) el rendimiento será similar al que conseguimos trayendo toda la lista en una sola query.

Resumiendo, si estás buscando un número pequeño de valores y posiblemente los consigas en las primeras iteraciones de la query paginada entonces la mejora en la eficiencia será obvia, en algunos casos he notado una media de mejora del 95%, pero ten en cuenta que si estas trayéndote la lista en trozos pequeños y lo más seguro es que tengas que procesar todos los elementos de la query el proceso será más lento, hasta un 400% más lento o incluso más.

No hay comentarios:

lunes, 4 de febrero de 2013

Iterando listas Grandes de SharePoint en trozos Pequeños

¿Por qué querría nadie iterar listas de SharePoint por partes? Por dos razones, principalmente. Primero porque si los resultados de una de tus queries son demasiados (por ejemplo 2000) el rendimiento baja mucho al trabajar con ellas y segundo porque quizás solo necesites trabajar con los primeros 20 elementos que cumplan una condición que es demasiado compleja para ser representada en una CAML query.

Yo estoy usand la paginación de queries para hacerlo y es un 75% más rápido que procesar todos los elementos que devuelve la primera query (esto depende también de la cantidad de datos que tengas en la lista)

Básicamente funciona así, pones el RowLimit a un valor prudente (queremos minimizar el número de queries que lanzamos) y luego haces las consultas a SharePoint hasta que el número de resultados llegue al objetivo que nos hemos marcado o te quedes sin elementos en la lita. (Todavía no he probado si es más rápido iterar sobre todos los elementos en una lista grande de golpe o hacerlo por partes pero algo me dice que de esta forma será más rápido)

/// <summary>
/// Returns the top MaxResults items in the query
/// </summary>
internal static List<SPListItem> GetTopItems(SPWeb web, uint MaxResults = 0)
{
    SPList ListToQuery = web.Lists.GetListByInternalName("ListInternalName");


    List<SPListItem> result = new List<SPListItem>();

    SPQuery query = new SPQuery();
    query.Query = string.Format(@"<Where><Eq><FieldRef Name='State' /><Value Type='Choice'>Live</Value></Eq></Where>");
    query.ViewAttributes = "Scope='RecursiveAll'";
    query.RowLimit = MaxResults + 1;

    SPListItemCollection QueryResults;
    string PagingInfo = string.Empty;

    while (result.Count < MaxResults && PagingInfo != null)
    {
        //We set the beginning of the query to the last item in the previous page
        query.ListItemCollectionPosition = new SPListItemCollectionPosition(PagingInfo);

        //We get the page of items
        QueryResults = ListToQuery.GetItems(query);

        foreach (SPListItem item in QueryResults)
        {
            //We process the items and add them to the result list
            if (ComplexProcessing(item))
                result.Add(item);
        }

        //We set PagingInfo to the last item retrieved in the query
        if (QueryResults.ListItemCollectionPosition != null)
            PagingInfo = QueryResults.ListItemCollectionPosition.PagingInfo.ToStringSafe();
        else //PagingInfo will be null if we reach the end of the pagination
            PagingInfo = null;
    }

    return result;
}

Este método de hacer consultas CAML paginadas ha incrementado enormemente el rendimiento de mi sitio, espero que también a ti te sirva.

No hay comentarios:

miércoles, 3 de octubre de 2012

Refrescando el SharePoint 2010 Timer Service mediante código

Necesito refrescar el servicio de Timer desde una aplicación y me ha costado bastante encontrar cómo hacerlo. Es tan facil como te imaginas

Aquí está el código:
/// 
/// Stops the SharePoint timer.
/// 
public static void TimerStop()
{
    ServiceController timerService = new ServiceController(Constants.SPTimerName);

    if (timerService.Status == ServiceControllerStatus.Running)
    {
        timerService.Stop();
        timerService.WaitForStatus(ServiceControllerStatus.Stopped, Constants.WaitingTimeout);
    }
}

/// 
/// Starts the SharePoint timer.
/// 
public static void TimerStart()
{
    ServiceController timerService = new ServiceController(Constants.SPTimerName);

    if (timerService.Status == ServiceControllerStatus.Stopped)
    {
        timerService.Start();
        timerService.WaitForStatus(ServiceControllerStatus.Running, Constants.WaitingTimeout);
    }
}
Por cierto, las constantes son:
public static TimeSpan WaitingTimeout = new TimeSpan(0, 1, 30);
public static string SPTimerName = "SharePoint 2010 Timer";

No hay comentarios:

jueves, 6 de septiembre de 2012

Autenticando un Web Service ASMX en SharePoint desde una aplicación WinRT

¿Dónde está el CookieContainer?

Sí amigo, yo también me he preguntado eso.

Estamos creando una aplicación para Windows 8 y necesitamos que se conecte a nuestros servicios web. Conectarse de manera anónima es fácil pero, cuando se trata de conectarse autenticando el usuario, la cosa cambia.

Yo estaba muy seguro , seguro del verbo ignorante y cándido,  de que podría usar el mismo método que usaba para crear aplicaciones de WP7. Lo he hecho mil veces y WP7 es más o menos lo mismo que WinRT así que ¿Quién habría pensado que no funcionaría?
Bien, WinRT no es lo mismo que WP7.

Después de crear un SPAuthBridge intenté añadir la cookie de autenticación a mi SoapClient como siempre pero, como probablemente sabes si estás leyendo este post, esta línea no funciona:

MySoapClient.CookieContainer = SharePointAuth.cookieJar;

En WinRT el objeto SoapClient no tiene CookieContainer asi que tenemos que buscarnos otra manera de añadir la cookie en las llamadas.

El SPAuthBridge va bien. Solo he añadido un método nuevo para obtener la cookie de autenticación del CookieContainer.
public string GetAuthenticationCookie()
{
    return string.Format("{0}={1}", "FedAuth",
            cookieJar.GetCookies(new System.Uri("https://www.mySite.com"))["FedAuth"].Value);
}

Después de eso, solo tienes que seguir los pasos del otro post para autenticarte y después de haberte autenticado correctamente en SharePoint tienes que cambiar la manera en la que añades la cookie a tu SoapClient.

Yo he encontrado dos formas:

La rápida

Cada vez que necesites llamar al servicio web necesitaras añadir la cookie al header y entonces llamar al servicio dentro del mismo OperationContextScope. Más o menos así:
using (new OperationContextScope(MyWS.InnerChannel))
{
    HttpRequestMessageProperty httpRequestProperty = new HttpRequestMessageProperty();

    httpRequestProperty.Headers[System.Net.HttpRequestHeader.Cookie] = SharePointAuth.GetAuthenticationCookie();

    OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;

    UserInfo = await MyWS.GetInfoForUserAsync();
}

Esto es fácil pero no muy legible y si tienes que llamar al web service unas cuantas veces, seguramente, te vas a aburrir de hacerlo así.

La elegante

En lugar de añadir la cookie cada vez podemos crearnos un Behaviour que se encargue de hacerlo automáticamente.

Para hacerlo necesitas crearte una clase CookieBehavior. Yo he copiado el 99% de la mía de aquí y es así:
class CookieBehaviour: IEndpointBehavior
{
    private string cookie;

    public CookieBehaviour(string cookie)
    {
        this.cookie = cookie;
    }

    public void AddBindingParameters(ServiceEndpoint serviceEndpoint,
        BindingParameterCollection bindingParameters) { }

    public void ApplyClientBehavior(ServiceEndpoint serviceEndpoint, ClientRuntime behavior)
    {
        behavior.ClientMessageInspectors.Add(new CookieMessageInspector(cookie));
    }

    public void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint,
        EndpointDispatcher endpointDispatcher) { }

    public void Validate(ServiceEndpoint serviceEndpoint) { }
}

public class CookieMessageInspector : IClientMessageInspector
{
    private string cookie;

    public CookieMessageInspector(string cookie)
    {
        this.cookie = cookie;
    }

    public void AfterReceiveReply(ref Message reply,
        object correlationState) { }

    public object BeforeSendRequest(ref Message request, IClientChannel channel)
    {
        HttpRequestMessageProperty httpRequestMessage;
        object httpRequestMessageObject;
        if (request.Properties.TryGetValue(HttpRequestMessageProperty.Name
            , out httpRequestMessageObject))
        {
            httpRequestMessage = httpRequestMessageObject as HttpRequestMessageProperty;
        }
        else
            httpRequestMessage = new HttpRequestMessageProperty();

        httpRequestMessage.Headers[System.Net.HttpRequestHeader.Cookie] = cookie;
        request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessage);

        return null;
    }
}

Cuando tenemos esta clase ya solo tememos que añadir el Behavior al servicio web en la inicialización y la cookie se añadirá solita cada vez.
public MyWSSoapClient InitializeMyWS()
{
    BasicHttpBinding Bind = new BasicHttpBinding();

    //////////////////////Transport///////////////////////
    Bind.Security.Mode = BasicHttpSecurityMode.Transport;
    //////////////////////////////////////////////////////

    /////////////////////MessageSize//////////////////////
    Bind.MaxReceivedMessageSize = Int32.MaxValue;
    //////////////////////////////////////////////////////

    EndpointAddress oAddress = new EndpointAddress(SiteUrl + "/_vti_bin/MyWS.asmx");
    MyWSSoapClient MyWS = new MyWSSoapClient(Bind, oAddress);

    CookieBehaviour AuthCookieBehaviour = new CookieBehaviour(SharePointAuth.GetAuthenticationCookie());
    MyWS.Endpoint.EndpointBehaviors.Add(AuthCookieBehaviour);

    return MyWS;
}

Después de haber inicializado el servicio web de esta manera puedes llamar a los métodos sin preocuparte de la autenticación nunca más:
private async Task<ObservableCollection<Info>> GetInfo()
{
    GetInfoForUserResponse Information = await MyWS.GetInfoForUserAsync();

    return Information.Body.GetInfoForUserResult;
}
En el método inicializador el poner el SecurityMode del BasicHttpBinding a Transport es necesario porque la aplicación web en la que se aloja el servicio web es HTTPS. Por defecto el SecurityMode está puesto a None y me estaba dando una ArgumentException como esta:

The provided URI scheme 'https' is invalid; expected 'http'.

El MaxReceivedMessageSize quizás podría ser una mijita excesivo. Configúralo para que se adapte a tus necesidades pero, ten en cuenta que si lo pones demasiado bajo te va a dar una CommunicationException diciendo que:

The maximum message size quota for incoming messages (65536) has been exceeded. To increase the quota, use the MaxReceivedMessageSize property on the appropriate binding element.

Diviértete con la tablet si es que la tienes, yo estoy todavía esperando la mía.

No hay comentarios:

martes, 14 de febrero de 2012

Métodos Extensores para Objetos Null

¿Es posible usar un método extensor en algo que es null?

Qué bonito sería poder hacer algo como:
if (!MyString.IsNull()) return MyString.ToString();
Leí hace unos cuantos años que hacer eso sería imposible porque, al ser el objeto null no podrías llamar al método o algo así… Pero en mi cabeza tenía sentido.

Así que me he estado conteniendo para no hacerlo todo este tiempo hasta hoy que me he sentido valiente.

Los métodos extensores:
public static bool IsNull(this string str)
{
    return str == null;
}

public static string ToStringSafe(this object obj)
{
    return (obj ?? string.Empty).ToString();
}
El método main obviamente todo buen programa de pruebas debe ser una aplicación de consola:
static void Main(string[] args)
{
    string str = null;

    if (str.IsNull())
        Console.WriteLine("It was null...");
    else
        Console.WriteLine("It wasn't null...");

    Console.WriteLine(str.ToStringSafe());

    Console.ReadKey();
}
¡Y funciona! Esto abre una nueva dimensión a mis recetas de espagueti.

El primer ejemplo útil que se me viene a la cabeza aparte del ToStringSafe y del ToInt, ToDateTime... ¡Mírame!, ¡No puedo parar! es:

/// 
/// Disposes the object if it's not null.
/// 
public static void DisposeSafe(this IDisposable DisposableObject)
{
    if (DisposableObject != null)
        DisposableObject.Dispose();
}
Con este método nunca más tendremos que preocuparnos de si el objeto es null o no. Todo se libera sin problemas.

A veces me asusta que me den tanta alegría estas cosas…

No hay comentarios:

jueves, 2 de febrero de 2012

Extendiendo, Overridando y Clases Base

Hace un par de días mi primo me preguntó sobre el significado de la palabra override. Intenté explicárselo con un par de ejemplos tontos y los dos sonaron bastante estúpidos… Bien, este es un ejemplo real.

Queremos crear web parts Silverlight y que todos ellos compartan las mismas propiedades básicas y el mismo método render. Así que extendiendo Microsoft.SharePoint.WebPartPages.WebPart crearemos un BaseSilverlightWebPart con estas características (overridando sí sí, has leído bien, overridando, nada de sobreescribiendo sobreescribir es irte a la clase base, borrar el metodo y escribir encimael método render de la clase WebPart) y después lo usaremos para crear otros web parts fácilmente.

La clase base quedó así:
    public abstract class BaseSilverlightWebpart : Microsoft.SharePoint.WebPartPages.WebPart
    {
        #region Web Part Properties
        [Personalizable(PersonalizationScope.Shared)]
        [WebBrowsable(true)]
        [System.ComponentModel.Category("Stratex")]
        [WebDisplayName("XAP List URL")]
        [Description("Select the URL of the XAP list.")]
        public string XAPListUrl { get; set; }
        #endregion

        #region Private Properties
        Dictionary<string, string> InitParams;
        #endregion

        #region Abstract
        /// <summary>
        /// Setup here the of the XAP you will use
        /// </summary>
        public abstract string XAPName { get; }

        /// <summary>
        /// Setup here the initial parameters you will use in your Silverlight Web Part
        /// </summary>
        public abstract void SetUpParameters();
        #endregion

        #region Methods
        public void AddParameter(string Name, string Value)
        {
            if (InitParams == null)
                InitParams = new Dictionary<string, string>();

            if (InitParams.ContainsKey(Name))
                InitParams[Name] = Value;
            else
                InitParams.Add(Name, Value);
        }
        #endregion

        #region Overrides
        protected override void CreateChildControls()
        {
            SetUpParameters();

            if (string.IsNullOrEmpty(XAPListUrl))
                XAPListUrl = string.Format("{0}/Lists/XAPLibrary/", SPContext.Current.Web.ServerRelativeUrl);

            //Sometimes when you create the web part it's 0px by 0px... ¬ ¬
            if (string.IsNullOrEmpty(Height)) Height = "150px";
            if (string.IsNullOrEmpty(Width)) Width = "150px";

            LiteralControl obj = new LiteralControl();
            obj.Text = "<object id='silverlightHost' height='" + Height + "' width='" + Width +
                @"' data='data:application/x-silverlight-2,' type='application/x-silverlight-2' style='display:block' class='ms-dlgDisable'>
                            <param name='Source' value='" + XAPListUrl + XAPName + @"' />
                            <param name='MinRuntimeVersion' value='3.0.40624.0' />
                            <param name='Background' value='#00FFFFFF' />
                            <param name='windowless' value='true' />
                            <param name='autoUpgrade' value='true' />
                            ";

            if (InitParams.Count > 0)
            {
                obj.Text +="<param name='initParams' value='";

                int i = 0;
                foreach (var param in InitParams)
                {
                    if (i++ == 0)
                        obj.Text += string.Format("{0}={1}", param.Key, param.Value);
                    else
                        obj.Text += string.Format(", {0}={1}", param.Key, param.Value);
                }

                obj.Text += @"' />
                ";
            }
            obj.Text += @"<a href='http://go.microsoft.com/fwlink/?LinkID=149156&v=3.0.40624.0' style='text-decoration: none;'>
                <img src='http://go.microsoft.com/fwlink/?LinkId=108181' alt='Click here to install Silverlight' style='border-style: none'/>
                </a>
                </object>";

            this.Controls.Add(obj);
        }
        #endregion
    }
Extendiéndola podemos crear un web part Silverlight fácilmente:
    public class DynamicStrategyMap : BaseSilverlightWebpart
    {
        #region Overrides
        public override string XAPName
        {
            get { return "StratexPointStrategyMap.xap"; }
        }

        public override void SetUpParameters()
        {
            AddParameter("site", HttpUtility.UrlEncode(SPContext.Current.Web.ServerRelativeUrl));
        }
        #endregion
    }
Fíjate que no tienes que overridar ¿otra vez? nada si no te hace falta. El método render es exactamente igual que en la base por lo que no necesitamos ni nombrarlo. La clase usará el método de su clase base automáticamente. Mira cuánto código estamos ahorrándonos de escribir otra vez, todas las propiedades, el render gordo de la clase base, etc. Solo cambiamos lo que necesitamos cambiar.
También podemos añadir propiedades nuevas como aquí:
    public class StratexHeartBeat : BaseSilverlightWebpart
    {
        #region WebPartProperties
        [Personalizable(PersonalizationScope.Shared)]
        [WebBrowsable(true)]
        [System.ComponentModel.Category("Stratex")]
        [WebDisplayName("NumberOfEvents")]
        public string NumberOfEvents { get; set; }

        [Personalizable(PersonalizationScope.Shared)]
        [WebBrowsable(true)]
        [System.ComponentModel.Category("Stratex")]
        [WebDisplayName("TimerLapse")]
        public string TimerLapse { get; set; }

        [Personalizable(PersonalizationScope.Shared)]
        [WebBrowsable(true)]
        [System.ComponentModel.Category("Stratex")]
        [WebDisplayName("Indicator Summary Url")]
        public string IndicatorSummaryUrl { get; set; }
        #endregion

        #region Overrides

        public override string XAPName
        {
            get { return "StratexHeartBeat.xap"; }
        }

        public override void SetUpParameters()
        {
            AddParameter("site", HttpUtility.UrlEncode(SPContext.Current.Web.Url));

            if (string.IsNullOrEmpty(IndicatorSummaryUrl))
                IndicatorSummaryUrl = string.Format("{0}/Lists/WebPartPages/IndicatorSummary.aspx", SPContext.Current.Web.ServerRelativeUrl);

            AddParameter("indicatorsummaryurl", IndicatorSummaryUrl);

            if (Common.ConvertToInt(NumberOfEvents) < 1) NumberOfEvents = "1";

            AddParameter("numberofnews", NumberOfEvents);

            if (Common.ConvertToInt(TimerLapse) < 1) TimerLapse = "1";

            AddParameter("timerlapse", TimerLapse);
        }
        #endregion
    }
O incluso podemos añadir más código al método render overridando el método render overridado en la clase base toma ya, doble tirabuzón, carpado y mortal hacia atrás de palabro inventado.
    public class Commentary : BaseSilverlightWebpart
    {
        #region Overrides
        public override string XAPName
        {
            get { return "Commentary.xap"; }
        }

        public override void SetUpParameters()
        {
            AddParameter("site", HttpUtility.UrlEncode(SPContext.Current.Web.ServerRelativeUrl));
        }

        protected override void CreateChildControls()
        {
            base.CreateChildControls();

            //We are using this code to be able to close the explorer window from Silverlight
            LiteralControl JavaScript = new LiteralControl();

            JavaScript.Text = @"<script type='text/javascript'>
                                function doCloseLocal() 
                                {
                                    var version = parseFloat(navigator.appVersion.split('MSIE')[1]);

                                    if (version >= 7) {window.open('', '_parent', ''); }

                                    else { window.opener = self;  }

                                    window.close();
                                }
                                </script>";
            this.Controls.Add(JavaScript);
        }

        #endregion
    }
Fíjate también que antes de añadir el nuevo control hemos llamado al render de la clase base para que el web part se renderice ¿otra? en la página.
Y esta es la pinta que tiene el override en la vida real. Ahora sí que tiene sentido… ¿o no?

No hay comentarios:

jueves, 12 de enero de 2012

Sobre Windows Phone 7 y la autenticación con SharePoint

Conectar el teléfono a nuestro entorno de SharePoint era mi proyecto de navidad, y me llevó un rato... Al final conseguí hacerlo así que he pensado en compartir mi alegría. Además, he leído un montón de blogs sobre el tema ¿todos los que hay? y quería aportar mi granito de arena.

Vamos al asunto. Me he creado una clase llamada SPAuthBridge partiend del código de la SDK que puede ser usada para realizar las conexiones con el servidor de SharePoint.
using System;
using System.IO;
using System.Net;
using System.Text;

namespace PhoneUtils.Code
{
    public class SPAuthBridge
    {
        #region Properties
        public CookieContainer cookieJar = new CookieContainer();

        string SiteUrl, User;
        string Password; //This should be securestring, but I don't think it's available in WP7
        #endregion

        #region Constructors
        public SPAuthBridge(string SiteUrl, string User, string Password)
        {
            this.SiteUrl = SiteUrl;
            this.User = User;
            this.Password = Password;
        }
        #endregion

        #region Public Methods
        public void Authenticate()
        {
            try
            {
                if (string.IsNullOrEmpty(SiteUrl)) throw new ArgumentOutOfRangeException("The SPAuthBridge was not properly initialized");

                System.Uri authServiceUri = new Uri(string.Format("{0}/_vti_bin/authentication.asmx", SiteUrl));

                HttpWebRequest spAuthReq = HttpWebRequest.Create(authServiceUri) as HttpWebRequest;
                spAuthReq.CookieContainer = cookieJar;
                spAuthReq.Headers["SOAPAction"] = "http://schemas.microsoft.com/sharepoint/soap/Login";
                spAuthReq.ContentType = "text/xml; charset=utf-8";
                spAuthReq.Method = "POST";

                //add the soap message to the request
                spAuthReq.BeginGetRequestStream(new AsyncCallback(spAuthReqCallBack), spAuthReq);
            }
            catch
            {
                TriggerOnAuthenticated(false);
            }
        }
        #endregion

        #region Private Methods
        private void spAuthReqCallBack(IAsyncResult asyncResult)
        {
            string envelope =
                    @"<?xml version=""1.0"" encoding=""utf-8""?>
                    <soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">
                      <soap:Body>
                        <Login xmlns=""http://schemas.microsoft.com/sharepoint/soap/"">
                          <username>{0}</username>
                          <password>{1}</password>
                        </Login>
                      </soap:Body>
                    </soap:Envelope>";

            UTF8Encoding encoding = new UTF8Encoding();
            HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState;
            Stream _body = request.EndGetRequestStream(asyncResult);
            envelope = string.Format(envelope, User, Password);
            byte[] formBytes = encoding.GetBytes(envelope);

            _body.Write(formBytes, 0, formBytes.Length);
            _body.Close();

            request.BeginGetResponse(new AsyncCallback(ResponseCallback), request);
        }

        private void ResponseCallback(IAsyncResult asyncResult)
        {
            try
            {
                HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState;
                HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult);
                Stream content = response.GetResponseStream();

                if (request != null && response != null)
                {
                    if (response.StatusCode == HttpStatusCode.OK)
                    {
                        using (StreamReader reader = new StreamReader(content))
                        {
                            //Put debugging code here
                            string _responseString = reader.ReadToEnd();
                            reader.Close();
                        }
                    }
                }

                //authentication complete
                TriggerOnAuthenticated(true);
            }
            catch
            {
                TriggerOnAuthenticated(false);
            }
        }
        #endregion

        #region Events
        public delegate void OnAuthenticatedHandler(bool Success);

        public event OnAuthenticatedHandler OnAuthenticated;

        protected virtual void TriggerOnAuthenticated(bool Success)
        {
            if (OnAuthenticated != null)
                OnAuthenticated(Success);
        }
        #endregion
    }
}

La manera de usar esto es bastante simple, creas el objeto SPAuthBridge y, llamas al método Authenticate y listo, algo así:

        StratexWP7SoapClient StratexWP7 = new StratexWP7SoapClient(); //This is my web service
        SPAuthBridge SharePointAuth;

        public MainPage()
        {
            InitializeComponent();

            (...)

            SharePointAuth = new SPAuthBridge(SiteUrl, Username, Password);
            SharePointAuth.OnAuthenticated += new SPAuthBridge.OnAuthenticatedHandler(SharePointAuth_OnAuthenticated);

            if (!string.IsNullOrEmpty(Password))
                SharePointAuth.Authenticate();
            else
                MessageBox.Show("The application should be configured before use.");
        }

        void SharePointAuth_OnAuthenticated(bool Success)
        {
            if (!Success)
            {
                Deployment.Current.Dispatcher.BeginInvoke(() =>
                    { MessageBox.Show("There was an error on the authentication procedure. Please check the configuration."); });

                return;
            }

            StratexWP7.CookieContainer = SharePointAuth.cookieJar; //This is all you have to do to connect your web service. \m/ O.O \m/

            HookEvents();

            RequestData();
        }

Es precioso cuando funciona…

clip_image002clip_image002[4]
(Por cierto, las gráficas son de AmCharts)

No hay comentarios:

miércoles, 11 de enero de 2012

Las Barras de Desplazamiento aparecen en SharePoint 2010 en los Silverlights

Si, cada vez que fijas la altura o la anchura del web part te muestra las horribles barras de desplazamiento.

Esta no es la primera vez que trato me encuentro con esto, pero creo que esta vez lo he resuelto para siempre o por lo menos hasta la nueva versión de SharePoint el año que viene…. El problema estaba en la forma en que estaba creando el objeto Silverlight. Estaba usando estilos CSS en línea. Era algo así:
<object id='silverlightHost' style='height: 600px; width: 350px; margin: 0; padding: 0;' data='data:application/x-silverlight-2,' type='application/x-silverlight-2'>

Después de un millón de experimentos basados en la técnica del ensayo / error no pienses jamás que soy capáz de crear un Web Part Silverlight de los OOTB de SharePoint 2010 y copiar el código he cambiado la primera línea del objeto a:
<object id='silverlightHost' height='600px' width='350px' data='data:application/x-silverlight-2,' type='application/x-silverlight-2' style='display:block' class='ms-dlgDisable'>
Y eso resuelve el problema.

No hay comentarios:

jueves, 8 de septiembre de 2011

Web Part Proveedor de Filtro con un árbol Silverlight en un ModalDialog

Empecé con esto hace un par de días... No quería hacerlo, porque sabría que iba a ser doloroso... pero me obligaron... Y entonces pensé que sería un post perfecto para el blog por lo de painful.

Lo que queríamos conseguir era filtrar elementos en un List View Web Part por Entidad. En nuestra solución tenemos una jerarquía de entidades por lo que pensamos que sería buena idea que el web part mostrase la jerarquía como un árbol.
Bien, así que necesitamos un web part con un TreeView que sea capaz de mandar la entidad seleccionada como filtro a un LVWP. Fantástico. Lo hice... Y no le gustó a nadie. Lo querían en Silverlight y no solo eso, lo querían en una ventana pop up. Sip, me cogieron, nunca había hecho nada parecido pero, ¿Quién dijo miedo?

Es un montón de código, la mayor parte feo así que solo postearé la parte interesante (básicamente la parte relacionada con la comunicación entre las páginas y el Silverlight) y las URLs de donde cogí las ideas.
Lo primero es poner a funcionar un Filter Provider Web Part. Para ello seguí las instrucciones de aquí. Primero lo intenté con un IWebPartRow, pero no era lo que yo quería así que cambié a ITransformableFilterValues. Esta parte es bastante simple así que no comentaré nada más.
La segunda parte es crear una ventana emergente. Después de googlear si googlear te parece un palabro raro deberías escucharme decir overridar o rollupear un rato me enconté con este post, que me pareció un buen sitio para empezar. Mi código en el web part terminó así:

        protected override void OnLoad(EventArgs e)
        {
            if (Page.IsPostBack)
            {
                if (!string.IsNullOrEmpty(GetFormValue("HiddenEntityName")))
                {
                    Page.Session["SelectedEntityName"] = Page.Request.Form["HiddenEntityName"];
                    Page.Session["SelectedEntityID"] = Page.Request.Form["HiddenEntityID"];

                    RenderHeader();

                    //SelectedEntityText.Text = string.Format("{0}  ", Page.Request.Form["HiddenEntityName"], Page.Request.Form["HiddenEntityID"]);
                }
                else
                    RenderHeader();
            }

            string CurrentWeb = SPContext.Current.Web.Url;
            string height = "500";
            string width = "500";
            string page = "/_layouts/stratex/EntityTree.aspx";

            // use next line for direct with  between  and  
            string scrp = @"
                            ";

            Type t = this.GetType();
            if (!Page.ClientScript.IsClientScriptBlockRegistered(t, "bindWebserviceToAutocomplete"))
                Page.ClientScript.RegisterClientScriptBlock(t, "bindWebserviceToAutocomplete", scrp);
        }
Tuve problemas trayéndome los valores del ModalDialog. No era capaz de encontrar los ID ni los Titles de los controles porque son creados dinámicamente. El truco que usé fue crear dos campos ocultos en el CreateChildControls:
        protected override void CreateChildControls()
        {
            base.CreateChildControls();

            ...

            Page.ClientScript.RegisterHiddenField("HiddenEntityName", "");
            Page.ClientScript.RegisterHiddenField("HiddenEntityID", "");

            ...
        }
Oye Chan, ¿Te has dado cuenta que podrías usar solo la sesión y olvidarte de los HiddenFields? No preguntes, te lo advierto…
La próxima parte es crear la página aspx para el modal dialog. La guardé en un directorio que me creé en _layouts. El código de la página quedó así:
<%@ Page Language="C#" Inherits="StratExFramework.EntityTree,StratExFramework,Version=2.2.0.0,Culture=neutral,PublicKeyToken=311246df7412ca98" %>

<html>
<head>
<title>Select Entity for filtering</title>
<script type='text/javascript'>
    function PassParameterAndClose(EntityName, EntityID) {

        window.returnValue = new Array( EntityName, EntityID) ;

        var version = parseFloat(navigator.appVersion.split('MSIE')[1]);
        if (version >= 7) 
            { window.open('', '_parent', ''); }
        else
            { window.opener = self; }

        window.close();
    }
</script>
</head>
<body></body>
</html>
También me creé una clase de code behind como puedes ver en la primera línea del aspx... :
    public class EntityTree : WebPartPage
    {
        string CurrentWeb;
        string SelectedEntityID;

        protected void Page_Load(object sender, System.EventArgs e)
        {
            CurrentWeb = Request.Params["CurrentWeb"];
            SelectedEntityID = Request.Params["SelectedEntityID"];
        }

        protected override void CreateChildControls()
        {
            base.CreateChildControls();

            string Source = CurrentWeb + "/Lists/XAPLibrary/SilverlightEntityTreeSelector.xap";
            string SilverlightHeight = "515";
            string SilverlightWidth = "500";

            LiteralControl obj = new LiteralControl();
            obj.Text = "<object id='silverlightHost' style='height: " + SilverlightHeight + "; width: " + SilverlightWidth + @"; margin: 0; padding: 0;' data='data:application/x-silverlight-2,' type='application/x-silverlight-2'>
                            <param name='Source' value='" + Source + @"' />
                            <param name='MinRuntimeVersion' value='3.0.40624.0' />
                            <param name='Background' value='#FFFFFFFF' />
                            <param name='initParams' value='" +
                                string.Format("{0}={1}", "site", HttpUtility.UrlEncode(CurrentWeb)) +
                                string.Format(", {0}={1}", "selectedentityid", HttpUtility.UrlEncode(SelectedEntityID)) +
                                @"' />
                            </object>";
            this.Controls.Add(obj);
        }

        public override void VerifyRenderingInServerForm(Control control)
        {
            return;
        }
    }
Lo que hago aquí es coger los parámetros del entorno en donde se ejecuta el web part y pasárselos al Silverlight. Uso el CurrentWeb para decirle a los web services del Silverlight cual es el contexto y el SelectedEntityID para resaltar la entidad que fue seleccionada la última vez que se abrió la ventana modal. Este es el código del Silverlight.
namespace SilverlightEntityTreeSelector
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();

            Tree.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(Tree_PropertyChanged);

            Tree.Show(string.Empty);
        }

        void Tree_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "SelectedEntityID")
                HtmlPage.Window.Invoke("PassParameterAndClose", Tree.SelectedEntityName, Tree.SelectedEntityID);
            else if (e.PropertyName == "TreeLoaded")
                if (Application.Current.Resources.Contains("selectedentityid"))
                    if (!string.IsNullOrEmpty(Application.Current.Resources["selectedentityid"] as string))
                        Tree.ChangeSelectedItemTo(Application.Current.Resources["selectedentityid"] as string);
        }
    }
}

Ya tenemos todos los componentes.
Esto funciona de la siguiente manera: Seleccionas una entidad en el árbol de Silverlight, luego el Silverlight llama a la función javascript de la ventana modal y que pasa los parámetros al web part padre y cierra el popup. Finalmente el web part manda el filtro al List View WebPar.
Si usas este código te faltarán algunos métodos, pero lo que quería compartir aquí es el método que he seguido para conseguirlo básicamente porque no quiero tener que volver a pensarlo si alguna vez me vuelven a pedir que haga algo parecido en el futuro.



Me encantaría enseñaros algunas fotos, pero los web parts no han sido retocados por las hábiles manos de Adam, nuestro diseñador, y se os podrían salir los ojos de las órbitas.

No hay comentarios:

jueves, 18 de agosto de 2011

Creando una aplicación Windows Forms en SharePoint 2010

En mi tónica habitual, creo que me he tropezado con cada posible problema antes de conseguir ejecutar mi aplicación.

Lo que en MOSS2007 era todo risas y alegría se ha convertido en SharePoint 2010 en una tarea que me llevó por lo menos una hora… Y tuve suerte porque en mi tónica habitual, otra vez he llegado bastante tarde a 2010 y la mayoría de los problemas han sido ya resueltos y comentados en los foros…

Bueno, contaré la experiencia, así no se me olvidan los pasos necesarios la próxima vez con un poco de suerte.

Añadí la Microsoft.SharePoint.dll como siempre a las referencias y empecé a teclear. Escribí mi aplicación de tres lineas (sólo quería una aplicación para ayudarme a programar otra cosa) y le di a F5.

“The type or namespace name 'SharePoint' does not exist in the namespace 'Microsoft' (are you missing an assembly reference?)”

Bien, no se me había olvidado nada, el problema era que tienes que apuntar la solución al .Net Framework 3.5…

image

Esa fue fácil. Había un par de sitios en donde puedes encontrar esa información. Vale, F5 otra vez.

“The Web application at http://localhost could not be found. Verify that you have typed the URL correctly. If the URL should be serving existing content, the system administrator may need to add a new request URL mapping to the intended application”


- ¡Oh venga ya! ¡Está ahí! ¿Por qué no lo encuentras? + Porque no me has apuntado a 64 bits y no estoy funcionando en modo nativo. - Ups, perdona. + No pasa nada, solo tienes que leer mis mensajes de error que te indicarán perfectamente lo que tienes que hacer.
image

Esta fue un poco más difícil de encontrar, pero después de cambiar eso, le di felizmente a F5 otra vez y… ¡Sorpresa!

“The Web application at http://localhost could not be found. Verify that you have typed the URL correctly. If the URL should be serving existing content, the system administrator may need to add a new request URL mapping to the intended application”


Si, el mismo error otra vez… Y ya no encontraba más información en internet… ¿Y si es un problema de permisos?
image

Arranqué el Visual Studio como administrador y funcionó. ¡Por fin!

No hay comentarios: