Social Icons

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.
Publicar un comentario

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.
Publicar un comentario