Social Icons

Mostrando entradas con la etiqueta C#. Mostrar todas las entradas
Mostrando entradas con la etiqueta C#. 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:

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, 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:

jueves, 18 de abril de 2013

Patrón Recoge Tu Cuarto Después de Jugar

¿Es esto un patrón?

En la mayoría de los casos quieres cambiar el valor de una variable y dejarla así pero en algunas ocasiones quieres cambiar el valor de la variable, hacer algo y ponerla como estaba.

Con este código es muy fácil y te aseguras de que no te vas a olvidar nunca.

La idea es crear una clase que implemente la interfaz IDisposable y guarde los valores en el estado en el que estaban cuando se creó y luego los ponga a lo que necesitemos. Después, en el Dispose, los cambiará otra vez a los valores iniciales:
public class ManageAllowUnsafeUpdates : IDisposable
{
    private readonly bool allowUnsafeUpdatesStatus;
    private readonly SPWeb Web;

    public ManageAllowUnsafeUpdates(SPWeb Web)
    {
        this.Web = Web;
        allowUnsafeUpdatesStatus = Web.AllowUnsafeUpdates;
        Web.AllowUnsafeUpdates = true;
    }

    public void Dispose()
    {
        Web.AllowUnsafeUpdates = allowUnsafeUpdatesStatus;
    }
}

Usarlo es así de simple:
using (new ManageAllowUnsafeUpdates(Web))
{
    ListItem[this.FieldName] = (String)value;
    ListItem.Update();
}

Y no te tienes que preocupar nunca más de si el valor estaba a true o false antes porque la clase se ocupará de todo por ti.

Este código es un poco tonto, pero creo que sirve bien para demostrar la idea.

No hay comentarios:

miércoles, 17 de abril de 2013

Capturando Excepciones Incapturables

Mi aplicación ha estado funcionando perfectamente hasta hoy. Ahora, cada vez que la lanzo revienta sin más información que el temido CLR20r3.


Description:

  Stopped working



Problem signature:

  Problem Event Name: CLR20r3

  Problem Signature 01: appName.exe

  Problem Signature 02: 1.0.0.0

  Problem Signature 03: 516eb9fb

  Problem Signature 04: mscorlib

  Problem Signature 05: 2.0.0.0

  Problem Signature 06: 4a27471d

  Problem Signature 07: 20c8

  Problem Signature 08: 100

  Problem Signature 09: N3CTRYE2KN3C34SGL4ZQYRBFTE4M13NB

  OS Version: 6.1.7600.2.0.0.274.10

  Locale ID: 2057


He leído en internet que no me pasa esto a mi solo.

Y lo más molesto es que cuando lo estoy debugando no falla...

Al final encontré un post con respuestas Handling “Unhandled Exceptions” in .NET 2.0.

Y saqué este código, que puse en el Main de la aplicación:
AppDomain.CurrentDomain.UnhandledException += delegate(object sender, UnhandledExceptionEventArgs e)
 {
  EventLog.WriteEntry("appName", string.Format("{0} – IsTerminating = {1}", e.ExceptionObject.ToString(), e.IsTerminating), EventLogEntryType.Error, 001);
 };
Entonces ejecuté la aplicación y reventó también, pero esta vez me dejó una hermosa excepción en el event viewer.

Si quieres saber lo que pasaba es que no era capaz de cargar una DLL que estaba esperando.

Gracias Mark.

No hay comentarios:

jueves, 4 de abril de 2013

Generador Dinámico de Objetos para Bloqueos

Algunas veces necesitar crear un objeto de bloqueo dinámicamente, como en el post que escribí ayer.

Cuando estás trayendo una propiedad del property bag no quieres bloquear todas las property bags de todas las webs, solo necesitas asegurarte de que la propiedad que con la que estás trabajando no ha cambiado desde que empezaste el bloqueo... y para eso hace falta una manera dinámica de crear bloqueos.

Empecé esta clase con un diccionario pero después de usarla un tiempo me di cuenta de que los diccionarios no son thread safe para lectura... así que lo cambié por una hash table.
public class Locks
{
    /// <summary>
    /// This hashtable will provide a lock for every different key in the cache. Hashtables are threadsafe for one writer many readers scenario
    /// </summary>
    private static Hashtable LocksCollection = new Hashtable();

    public static object GetLock(string Key, params object[] args)
    {
        if (args!=null)
            Key = string.Format(Key, args);
           
        if (!LocksCollection.ContainsKey(Key))
        {
            lock (LocksCollection)
                if (!LocksCollection.ContainsKey(Key))
                    LocksCollection.Add(Key, new object());
        }

        return LocksCollection[Key];
    }
}

Con esta nueva clase en el método para guardar web properties solo bloqueamos la property bag en la que estamos trabajando, dejando las otras webs del servidor desbloqueadas, el código queda así:
/// <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 (Locks.GetLock("WebPropertyWriteLock - {0}", Web.ID))
    {
        if (GetWebPropertyThreadSafe(Web, Key) != Value)
        {
            Web.Properties[Key] = Value;

            Web.Properties.Update();
        }
    }
}

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, 10 de diciembre de 2012

ObservableDictionary para Silverlight

He reutilizado copiado el código del Blog de Shimmy pero he tenido que modificarlo un poco para que encajase en Silverlight. De momento solo lo he usado en uno de mis proyectos pero funciona bastante bien.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;

namespace SLUtils.Classes
{
        public class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged
        {
            private const string CountString = "Count";
            private const string IndexerName = "Item[]";
            private const string KeysName = "Keys";
            private const string ValuesName = "Values";

            private IDictionary<TKey, TValue> _Dictionary;
            protected IDictionary<TKey, TValue> Dictionary
            {
                get { return _Dictionary; }
            }

            #region Constructors
            public ObservableDictionary()
            {
                _Dictionary = new Dictionary<TKey, TValue>();
            }
            public ObservableDictionary(IDictionary<TKey, TValue> dictionary)
            {
                _Dictionary = new Dictionary<TKey, TValue>(dictionary);
            }
            public ObservableDictionary(IEqualityComparer<TKey> comparer)
            {
                _Dictionary = new Dictionary<TKey, TValue>(comparer);
            }
            public ObservableDictionary(int capacity)
            {
                _Dictionary = new Dictionary<TKey, TValue>(capacity);
            }
            public ObservableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer)
            {
                _Dictionary = new Dictionary<TKey, TValue>(dictionary, comparer);
            }
            public ObservableDictionary(int capacity, IEqualityComparer<TKey> comparer)
            {
                _Dictionary = new Dictionary<TKey, TValue>(capacity, comparer);
            }
            #endregion

            #region IDictionary<TKey,TValue> Members

            public void Add(TKey key, TValue value)
            {
                Insert(key, value, true);
            }

            public bool ContainsKey(TKey key)
            {
                return Dictionary.ContainsKey(key);
            }

            public ICollection<TKey> Keys
            {
                get { return Dictionary.Keys; }
            }

            public bool Remove(TKey key)
            {
                if (key == null) throw new ArgumentNullException("key");

                TValue value;
                Dictionary.TryGetValue(key, out value);

                int index = GetIndex(Dictionary, key);
                var removed = Dictionary.Remove(key);

                if (removed)
                    OnCollectionChanged(NotifyCollectionChangedAction.Remove, new KeyValuePair<TKey, TValue>(key, value), index);
                    //OnCollectionChanged();
                return removed;
            }

            public bool TryGetValue(TKey key, out TValue value)
            {
                return Dictionary.TryGetValue(key, out value);
            }

            public ICollection<TValue> Values
            {
                get { return Dictionary.Values; }
            }

            public TValue this[TKey key]
            {
                get
                {
                    return Dictionary[key];
                }
                set
                {
                    Insert(key, value, false);
                }
            }

            #endregion

            #region ICollection<KeyValuePair<TKey,TValue>> Members

            public void Add(KeyValuePair<TKey, TValue> item)
            {
                Insert(item.Key, item.Value, true);
            }

            public void Clear()
            {
                if (Dictionary.Count > 0)
                {
                    Dictionary.Clear();
                    OnCollectionChanged();
                }
            }

            public bool Contains(KeyValuePair<TKey, TValue> item)
            {
                return Dictionary.Contains(item);
            }

            public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
            {
                Dictionary.CopyTo(array, arrayIndex);
            }

            public int Count
            {
                get { return Dictionary.Count; }
            }

            public bool IsReadOnly
            {
                get { return Dictionary.IsReadOnly; }
            }

            public bool Remove(KeyValuePair<TKey, TValue> item)
            {
                return Remove(item.Key);
            }

            #endregion

            #region IEnumerable<KeyValuePair<TKey,TValue>> Members

            public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
            {
                return Dictionary.GetEnumerator();
            }

            #endregion

            #region IEnumerable Members

            IEnumerator IEnumerable.GetEnumerator()
            {
                return ((IEnumerable)Dictionary).GetEnumerator();
            }

            #endregion

            #region INotifyCollectionChanged Members

            public event NotifyCollectionChangedEventHandler CollectionChanged;

            #endregion

            #region INotifyPropertyChanged Members

            public event PropertyChangedEventHandler PropertyChanged;

            #endregion

            public void AddRange(IDictionary<TKey, TValue> items)
            {
                if (items == null) throw new ArgumentNullException("items");

                if (items.Count > 0)
                {
                    if (Dictionary.Count > 0)
                    {
                        if (items.Keys.Any((k) => Dictionary.ContainsKey(k)))
                            throw new ArgumentException("An item with the same key has already been added.");
                        else
                            foreach (var item in items) Dictionary.Add(item);
                    }
                    else
                        _Dictionary = new Dictionary<TKey, TValue>(items);

                    OnCollectionChanged(NotifyCollectionChangedAction.Add, items.ToArray(), GetIndex(Dictionary, items.Keys.First())); //We notify the index of the first added key
                }
            }

            private void Insert(TKey key, TValue value, bool add)
            {
                if (key == null) throw new ArgumentNullException("key");

                TValue item;
                if (Dictionary.TryGetValue(key, out item))
                {
                    if (add) throw new ArgumentException("An item with the same key has already been added.");
                    if (Equals(item, value)) return;
                    Dictionary[key] = value;

                    OnCollectionChanged(NotifyCollectionChangedAction.Replace, new KeyValuePair<TKey, TValue>(key, value), new KeyValuePair<TKey, TValue>(key, item), GetIndex(Dictionary, key));
                }
                else
                {
                    Dictionary[key] = value;

                    OnCollectionChanged(NotifyCollectionChangedAction.Add, new KeyValuePair<TKey, TValue>(key, value), GetIndex(Dictionary, key));
                }
            }

            private void OnPropertyChanged()
            {
                OnPropertyChanged(CountString);
                OnPropertyChanged(IndexerName);
                OnPropertyChanged(KeysName);
                OnPropertyChanged(ValuesName);
            }

            private int GetIndex(IDictionary<TKey, TValue> Dictionary, TKey key)
            {
                int i = 0;
                foreach (var dictKey in Dictionary.Keys)
                {
                    if (dictKey.Equals(key)) return i;
                }

                return -1;
            }

            protected virtual void OnPropertyChanged(string propertyName)
            {
                if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }

            private void OnCollectionChanged()
            {
                OnPropertyChanged();
                if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            }

            private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> changedItem, int index)
            {
                OnPropertyChanged();
                if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, changedItem, index));
            }

            private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> newItem, KeyValuePair<TKey, TValue> oldItem, int index)
            {
                OnPropertyChanged();
                if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
            }

            private void OnCollectionChanged(NotifyCollectionChangedAction action, IList newItems, int index)
            {
                OnPropertyChanged();
                if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItems, index));
            }

            public override string ToString()
            {
                return string.Format("ObservableDictionary<{0}> Count={1}", GetKeyAndValue(Dictionary.GetType().GetGenericArguments()), Count);
            }

            private object GetKeyAndValue(Type[] Types)
            {
                string result = string.Empty;

                foreach (Type type in Types)
                {
                    result += type.ToString() + ",";
                }

                if (!string.IsNullOrEmpty(result) && result.Length > 1)
                    result = result.Substring(0, result.Length - 1);

                return result;
            }
        }
}
Pruébalo y dime cómo va.

No hay comentarios:

jueves, 15 de noviembre de 2012

Click funciona pero Tapped causa error en Windows 8

Nos han rechazado la app de la tienda de Windows 8 porque peta cuando el usuario toca o hace Tap o pone su zarpa en una grid view. Como no tenemos una surface estábamos haciendo las pruebas en el simulador, pero con la herramienta del ratón y no con la del dedo y funcionaba perfectamente…

Se cuelga cada vez que tocas en un elemento dentro de una GridView y es curioso porque se cuelga justo en el momento en el que tocas, el código ni siguiera llega a ejecutar el callback. La información del cuelgue tampoco nos da información útil, como era de esperar.

Parece que hay un error en la GridView y tras ser asignada a un DataContext con datos necesita ser refrescada antes de aceptar Taps, aunque funciona bien con los Clicks.

No hay forma o yo no la he encontrado de refrescar el UI en WinRT, lo que creo que resolvería el problema, así que he tomado dos caminos diferentes para tratar de evitar el fallo

  • Informes de carga rápida: Estoy pre-cargando los contenidos de los informes que cargan rápido cuando se ejecuta la app. Haciendo esto relleno todas las GridViews en páginas diferentes de las que el usuario está viendo, en segundo plano. Una vez que se selecciona la página en donde reside la GridView ésta es renderizada junto con el resto de la página y se solventa el problema.
  • Informes de carga lenta: También pre-cargo el contenido de estos informes, pero si el usuario los muestra antes de que se rellene la GridView con lo que los datos se mostrarían pero el control no se refrescaría y el Tapped seguiría fallando. Para evitar esto he cambiado también el comportamiento de la carga de datos. Asigno el DataContext antes y lo actualizo varias veces, una vez por cada paquete de información que recibo. Así el GridView se refresca unas cuantas veces y se soluciona el problema con el Tapped.

Los chavales del Windows Store App Lab han dicho que van a buscar una solución al problema pero yo creo que este es uno de esos errores que se arregla con un paquete en el Windows Update… Os mantendré informados si se inventan algo nuevo.


***ACTUALIZACIÓN***
Un experto de Microsoft me ha apuntado que si creas y añades los datos a la GridView en el código y después la añades a la página funciona correctamente. Lo he probado y es verdad.

Los templates en mis GridViews son complejos así que haré UserControls con las GridViews de manera que pueda crearlas con datos en código sin perder la posibilidad de seguir usando MVVM. Esto todavía no lo he probado.

No hay comentarios:

miércoles, 3 de octubre de 2012

Un Thread.Sleep que no cuelga el hilo

He estado añadiendo tests unitarios a mi código de SharePoint y no es una tarea facil.

Una de las cosas más importantes que quería probar eran unos ItemEventReceivers de una lista. Esto funciona sin problemas cuando pruebas los eventos síncronos como el ItemAdding o el ItemUpdating pero cuando se trata de probar los eventos asíncronos es necesario esperar...

Si pones el hilo del test a dormir los eventos asíncronos no se ejecutarán ya que es el mismo en el que se ejecuta el código de SharePoint y aunque tu código funcione cuando lo ejecutas a mano los tests fallarán.

Es por esto que he creado una clase basada en timers que no cuelga el hilo dejando ejecutarse los eventos asíncronos.
public class Waiter : IDisposable
{
    public enum WaiterState
    {
        Waiting,
        TimedOut,
        Success,
        Error
    };

    System.Timers.Timer WaitTimer;
    ManualResetEvent manualResetEvent;
    int WaitCounter;

    private Waiter(int interval)
    {
        WaitCounter = 0;

        manualResetEvent = new ManualResetEvent(true);
        WaitTimer = new System.Timers.Timer() { AutoReset = false, Interval = interval };
        WaitTimer.Elapsed += new ElapsedEventHandler(WaitTimer_Elapsed);
    }

    void WaitTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
        WaitCounter++;
        manualResetEvent.Set();
    }

    /// 
    /// Waits for the interval in milliseconds times number of times or once by default.
    /// 
    public static WaiterState Wait(int interval, int times)
    {
        try
        {
            using (Waiter WaiterClass = new Waiter(interval))
            {
                while (WaiterClass.WaitCounter <= times)
                {
                    WaiterClass.WaitTimer.Start();
                    WaiterClass.manualResetEvent.WaitOne();
                }
            }
        }
        catch
        {
            return WaiterState.Error;
        }

        return WaiterState.Success;
    }

    /// 
    /// Waits for the interval in milliseconds once.
    /// 
    public static WaiterState Wait(int interval)
    {
        return Wait(interval, 0);
    }

    void Dispose()
    {
        WaitTimer.Dispose();
    }
}
Pruébala.

No hay comentarios:

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:

miércoles, 5 de septiembre de 2012

Snippet para DependencyProperty con método Call-Back y valor por defecto

Para mi esta es la manera en la que Microsoft debería haber hecho el snippet de DependencyProperty desde el principio.
Uso este un montón y tiene definidos lugares para el valor por defecto, el método call-back e incluso asigna la instancia de la clase que llama al call-back a una variable automaticamente.
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
 <CodeSnippet Format="1.0.0">
  <Header>
   <Title>Dependency Property With CallBack</Title>
   <Shortcut>propdpCallBack</Shortcut>
   <Description>Creates a dependency property with it's callback method</Description>
   <Author>Jose Sanchez</Author>
   <SnippetTypes>
    <SnippetType>Expansion</SnippetType>
    <SnippetType>SurroundsWith</SnippetType>
   </SnippetTypes>
  </Header>
  <Snippet>
   <Declarations>
    <Literal>
     <ID>PublicName</ID>
     <Default>PropertyName</Default>
     <ToolTip>Public name of the depencency property</ToolTip>
    </Literal>
    <Literal>
     <ID>type</ID>
     <Default>string</Default>
     <ToolTip>Type of the property</ToolTip>
    </Literal>
    <Literal>
     <ID>ParentClassType</ID>
     <Default>UserControl</Default>
     <ToolTip>Type of the parent class of the dependency property</ToolTip>
    </Literal>
    <Literal>
     <ID>DefaultValue</ID>
     <Default>string.Empty</Default>
     <ToolTip>Default value of the dependency property</ToolTip>
    </Literal>
    <Literal>
     <ID>CallBackName</ID>
     <Default>PropertyCallBackName</Default>
     <ToolTip>Name of the callback method that will be triggered when the depencency property has changed</ToolTip>
    </Literal>
   </Declarations>
   <Code Language="csharp"><![CDATA[public $type$ $PublicName$
        {
            get { return ($type$)GetValue($PublicName$Property); }
            set { SetValue($PublicName$Property, value); }
        }

        // Using a DependencyProperty as the backing store for $PublicName$.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty $PublicName$Property =
            DependencyProperty.Register("$PublicName$", typeof($type$), typeof($ParentClassType$), new PropertyMetadata($DefaultValue$, new PropertyChangedCallback($CallBackName$)));


 private static void $CallBackName$(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            $ParentClassType$ Changed$ParentClassType$ = (obj as $ParentClassType$);

            //TODO: Implement some actions in Changed$ParentClassType$
        }]]>
   </Code>
  </Snippet>
 </CodeSnippet>
</CodeSnippets>
Espero que os sea tan útil como a mi.

Por cierto, podéis encontrar más información sobre cómo crear e importar snippets aquí.

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:

viernes, 6 de enero de 2012

Creando Eventos en Silverlight

He leído que es imposible en un par de sitios. No se si lo sería en las versiones anteriores pero, por lo menos desde Silverlight 3 se puede… y digo más… es fácil.

Este evento lo uso para comprobar cuándo se ha movido un objeto.

        #region Events
        public delegate void PositionChangeHandler(Object Sender);

        public event PositionChangeHandler PositionChangedEvent;

        protected virtual void OnPositionChanged()
        {
            if (PositionChangedEvent != null)
                PositionChangedEvent(this);
        }
        #endregion

Y entonces en la función en donde actualizo la posición del objeto llamo a:
OnPositionChanged();

Después de esto solo tengo que suscribirme al evento desde las otras clases:
ParentObjective.PositionChangedEvent += new Objective.PositionChangeHandler(RelatedObjectives_PositionChangedEvent);

Fácil… por una vez en la vida

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: