Może zainteresują Cię pozostałe posty z cyklu Mapowanie SPListItem na obiekt:
- Mapowanie SPListItem na obiekt - Wprowadzenie
- Mapowanie SPListItem na obiekt - Wrapper (aktualnie czytasz)
- Mapowanie SPListItem na obiekt - implicit/explicit conversion
- Mapowanie SPListItem na obiekt - LINQ to SharePoint
- Mapowanie SPListItem na obiekt - AutoMapper
- Mapowanie SPListItem na obiekt - Field Mapper
W poprzedniej części opisałem sposób dostępu do obiektu SPListItem za pomocą braku mapowania - dostęp odbywał się bezpośrednio przez obiekt SPListItem. W drugiej części opiszę to, co zastosowaliśmy w jednym z niesławnych projektów w 2007 roku.
W wspomnianym projekcie, jak tylko zorientowaliśmy się, że SharePoint nie posiada silnie typowanych pól, stwierdziliśmy, że trzeba coś z tym zrobić. Jako że czas gonił a na temat SPS w tych czasach prawie w ogóle postów nie było wymyśliliśmy najszybszy sposób silnie typowanego dostępu do danych jaki mogliśmy wykonać w krótkim czasie - wrapper. Cały kod i przykłady użyte w postcie pochodzą z okresu 2007 roku. Chciałbym to po prostu zaznaczyć :)
Wrapper
Mówiąc krótko, wrapper opakowuje dostęp do elementów poprzez zunifikowany interfejs starając się ukryć poorly designed or complicated interface.
To co chcieliśmy osiągnąć to podmienić wywołanie SPListItem[„Name”] na ClassName.Name a przy tym udostępnić prosty, zunifikowany dostęp do danych. Nie interesował nas przy tym typ zwracanych danych - to znaczy, mogliśmy żyć z tym iż zostanie nam zwrócony obiekt SPFieldLookupValue czy SPFieldUserValue, zaś co do pozostałych to chcieliśmy by dane zwrócone były value type.
Czyli do prostej listy z tytułem (pole wymagane) oraz liczbą (pole niewymagane) chcieliśmy mieć następujący dostęp:
public partial interface IItemEntity
{
int Id { get; }
Guid UniqueId { get; }
DateTime Created { set; get; }
DateTime Modified { set; get; }
SPAttachmentCollection Attachments { get; }
string Title { set; get; }
SPFieldUserValue Author { set; get; }
double? Number { set; get; }
SPFieldUserValue Editor { set; get; }
}
Także chcieliśmy mieć możliwość tworzenia, aktualizacji i usuwania obiektów za pomocą wrappera:
public partial class ItemEntity : IItemEntity
{
private SPListItem _spListItem;
public ItemEntity(SPListItem listItem)
{
if(listItem == null)
{
throw new ArgumentNullException("listItem");
}
this._spListItem = listItem;
}
public ItemEntity()
{
SPSite spSite = SPContext.Current.Site;
SPWeb spWeb = spSite.OpenWeb(ItemEntityConsts.Web);
SPList spList = spWeb.Lists[ItemEntityConsts.List];
this._spListItem = spList.Items.Add();
}
public ItemEntity(SPWeb web)
{
if(web == null)
{
throw new ArgumentNullException("web");
}
SPSite spSite = web.Site;
SPWeb spWeb = spSite.OpenWeb(ItemEntityConsts.Web);
SPList spList = spWeb.Lists[ItemEntityConsts.List];
this._spListItem = spList.Items.Add();
}
public void Update()
{
this._spListItem.Update();
}
public void Delete()
{
this._spListItem.Delete();
}
// ... usuniete properties
}
Jednakże sam wrapper nie dawał nam pełnych możliwości, musieliśmy dorobić jeszcze data access layer, który udostępniał nam metody operujące na obiekcie lub kolekcji obiektów - teraz taki layer można by nazwać repozytorium.
public partial interface IItemDataAccess
{
ICollection<IItemEntity> Items { get; }
ICollection<IItemEntity> GetItemsByQuery(string query);
ICollection<IItemEntity> GetItemsByQuery(string query, SPWeb web);
ICollection<IItemEntity> GetItemsByQuery(string query, int limit);
ICollection<IItemEntity> GetItemsByQuery(string query, int limit, SPWeb web);
IItemEntity GetItemById(int id);
IItemEntity GetItemById(int id, SPWeb web);
IItemEntity GetItemByUniqueId(Guid uniqueId);
IItemEntity GetItemByUniqueId(Guid uniqueId, SPWeb web);
IItemEntity CreateItemInstance();
IItemEntity CreateItemInstance(SPWeb web);
IItemEntity CreateItemInstance(SPListItem listItem);
}
Podsumowując, całość interfejsów wyglądała tak:
// IDki pol
internal static class ItemEntityConsts
{
internal static Guid Title = new Guid("fa564e0f-0c70-4ab9-b863-0177e6ddd247");
internal static Guid Author = new Guid("1df5e554-ec7e-46a6-901d-d85a3881cb18");
internal static Guid Number = new Guid("342bae15-79ae-4773-8039-7ff651579928");
internal static Guid Editor = new Guid("d31655d1-1d5b-4511-95a1-7a09e9b75bf2");
internal static string Created = "Created";
internal static string Modified = "Modified";
internal static string Web = "/";
internal static string List = "Items";
}
// interfejs dla entity
public partial interface IItemEntity
{
int Id { get; }
Guid UniqueId { get; }
DateTime Created { set; get; }
DateTime Modified { set; get; }
SPAttachmentCollection Attachments { get; }
string Title { set; get; }
SPFieldUserValue Author { set; get; }
double? Number { set; get; }
SPFieldUserValue Editor { set; get; }
void Update();
void Delete();
}
// interfejs dostepu do danych
public partial interface IItemDataAccess
{
ICollection<IItemEntity> Items { get; }
ICollection<IItemEntity> GetItemsByQuery(string query);
ICollection<IItemEntity> GetItemsByQuery(string query, SPWeb web);
ICollection<IItemEntity> GetItemsByQuery(string query, int limit);
ICollection<IItemEntity> GetItemsByQuery(string query, int limit, SPWeb web);
IItemEntity GetItemById(int id);
IItemEntity GetItemById(int id, SPWeb web);
IItemEntity GetItemByUniqueId(Guid uniqueId);
IItemEntity GetItemByUniqueId(Guid uniqueId, SPWeb web);
IItemEntity CreateItemInstance();
IItemEntity CreateItemInstance(SPWeb web);
IItemEntity CreateItemInstance(SPListItem listItem);
}
Implementacja ItemEntity tak:
public partial class ItemEntity : IItemEntity
{
private SPListItem _spListItem;
public ItemEntity(SPListItem listItem)
{
if (listItem == null)
{
throw new ArgumentNullException("listItem");
}
this._spListItem = listItem;
}
public ItemEntity()
{
SPSite spSite = SPContext.Current.Site;
SPWeb spWeb = spSite.OpenWeb(ItemEntityConsts.Web);
SPList spList = spWeb.Lists[ItemEntityConsts.List];
this._spListItem = spList.Items.Add();
}
public ItemEntity(SPWeb web)
{
if (web == null)
{
throw new ArgumentNullException("web");
}
SPSite spSite = web.Site;
SPWeb spWeb = spSite.OpenWeb(ItemEntityConsts.Web);
SPList spList = spWeb.Lists[ItemEntityConsts.List];
this._spListItem = spList.Items.Add();
}
public void Update()
{
this._spListItem.Update();
}
public void Delete()
{
this._spListItem.Delete();
}
public int Id
{
get { return this._spListItem.ID; }
}
public Guid UniqueId
{
get { return this._spListItem.UniqueId; }
}
public DateTime Created
{
set { this._spListItem[ItemEntityConsts.Created] = value; }
get { return (DateTime)this._spListItem[ItemEntityConsts.Created]; }
}
public DateTime Modified
{
set { this._spListItem[ItemEntityConsts.Modified] = value; }
get { return (DateTime)this._spListItem[ItemEntityConsts.Modified]; }
}
public SPAttachmentCollection Attachments
{
get { return this._spListItem.Attachments; }
}
public string Title
{
set
{
if(string.IsNullOrEmpty(value))
{
throw new ArgumentNullException("value");
}
this._spListItem[ItemEntityConsts.Title] = value;
}
get
{
try
{
return (string)this._spListItem[ItemEntityConsts.Title];
}
catch(InvalidCastException ex)
{
string message = string.Format(CultureInfo.InvariantCulture, ExceptionMessages.EntityStrongTypingException, "ItemEntityConsts.Title", "Items");
throw new System.Data.StrongTypingException(message, ex);
}
}
}
public SPFieldUserValue Author
{
set
{
this._spListItem[ItemEntityConsts.Author] = value;
}
get
{
try
{
if(this._spListItem[ItemEntityConsts.Author] != null)
{
return new SPFieldUserValue(this._spListItem.Web, this._spListItem[ItemEntityConsts.Author].ToString());
}
else
{
return null;
}
}
catch(InvalidCastException ex)
{
string message = string.Format(CultureInfo.InvariantCulture, ExceptionMessages.EntityStrongTypingException, "ItemEntityConsts.Author", "Items");
throw new System.Data.StrongTypingException(message, ex);
}
}
}
public double? Number
{
set
{
this._spListItem[ItemEntityConsts.Number] = value;
}
get
{
try
{
return (double?)this._spListItem[ItemEntityConsts.Number];
}
catch(InvalidCastException ex)
{
string message = string.Format(CultureInfo.InvariantCulture, ExceptionMessages.EntityStrongTypingException, "ItemEntityConsts.Number", "Items");
throw new System.Data.StrongTypingException(message, ex);
}
}
}
public SPFieldUserValue Editor
{
set
{
this._spListItem[ItemEntityConsts.Editor] = value;
}
get
{
try
{
if(this._spListItem[ItemEntityConsts.Editor] != null)
{
return new SPFieldUserValue(this._spListItem.Web, this._spListItem[ItemEntityConsts.Editor].ToString());
}
else
{
return null;
}
}
catch(InvalidCastException ex)
{
string message = string.Format(CultureInfo.InvariantCulture, ExceptionMessages.EntityStrongTypingException, "ItemEntityConsts.Editor", "Items");
throw new System.Data.StrongTypingException(message, ex);
}
}
}
}
Implementacja Data Access zaś tak:
public partial class ItemDataAccess : IItemDataAccess
{
public ICollection<IItemEntity> Items
{
get { return this.GetItemsByQuery("", -1); }
}
public ICollection<IItemEntity> GetItemsByQuery(string query)
{
return this.GetItemsByQuery(query, -1);
}
public ICollection<IItemEntity> GetItemsByQuery(string query, SPWeb web)
{
return this.GetItemsByQuery(query, -1, web);
}
public ICollection<IItemEntity> GetItemsByQuery(string query, int limit)
{
ICollection<IItemEntity> list = new Collection<IItemEntity>();
SPSite spSite = SPContext.Current.Site;
using(SPWeb spWeb = spSite.OpenWeb(ItemEntityConsts.Web))
{
SPList spList = spWeb.Lists[ItemEntityConsts.List];
SPQuery spQuery = new SPQuery();
spQuery.Query = query;
if(limit >= 0)
{
spQuery.RowLimit = (uint)limit;
}
SPListItemCollection listItems = spList.GetItems(spQuery);
foreach(SPListItem listItem in listItems)
{
list.Add(new ItemEntity(listItem));
}
return list;
}
}
public ICollection<IItemEntity> GetItemsByQuery(string query, int limit, SPWeb web)
{
if(web == null)
{
throw new ArgumentNullException("web");
}
ICollection<IItemEntity> list = new Collection<IItemEntity>();
SPSite spSite = web.Site;
using(SPWeb spWeb = spSite.OpenWeb(ItemEntityConsts.Web))
{
SPList spList = spWeb.Lists[ItemEntityConsts.List];
SPQuery spQuery = new SPQuery();
spQuery.Query = query;
if(limit >= 0)
spQuery.RowLimit = (uint) limit;
SPListItemCollection listItems = spList.GetItems(spQuery);
foreach(SPListItem listItem in listItems)
{
list.Add(new ItemEntity(listItem));
}
return list;
}
}
public IItemEntity GetItemById(int id)
{
SPSite spSite = SPContext.Current.Site;
using(SPWeb spWeb = spSite.OpenWeb(ItemEntityConsts.Web))
{
SPList spList = spWeb.Lists[ItemEntityConsts.List];
return new ItemEntity(spList.GetItemById(id));
}
}
public IItemEntity GetItemById(int id, SPWeb web)
{
if(web == null)
{
throw new ArgumentNullException("web");
}
SPSite spSite = web.Site;
using(SPWeb spWeb = spSite.OpenWeb(ItemEntityConsts.Web))
{
SPList spList = spWeb.Lists[ItemEntityConsts.List];
return new ItemEntity(spList.GetItemById(id));
}
}
public IItemEntity GetItemByUniqueId(Guid uniqueId)
{
SPSite spSite = SPContext.Current.Site;
using(SPWeb spWeb = spSite.OpenWeb(ItemEntityConsts.Web))
{
SPList spList = spWeb.Lists[ItemEntityConsts.List];
return new ItemEntity(spList.GetItemByUniqueId(uniqueId));
}
}
public IItemEntity GetItemByUniqueId(Guid uniqueId, SPWeb web)
{
if(web == null)
{
throw new ArgumentNullException("web");
}
SPSite spSite = web.Site;
using(SPWeb spWeb = spSite.OpenWeb(ItemEntityConsts.Web))
{
SPList spList = spWeb.Lists[ItemEntityConsts.List];
return new ItemEntity(spList.GetItemByUniqueId(uniqueId));
}
}
public IItemEntity CreateItemInstance()
{
return new ItemEntity();
}
public IItemEntity CreateItemInstance(SPWeb web)
{
return new ItemEntity(web);
}
public IItemEntity CreateItemInstance(SPListItem listItem)
{
return new ItemEntity(listItem);
}
}
Wady
- Rozwiązanie jest dość ciężkie, i działa na obiektach SharePoint cały czas;
- Przy posiadaniu własnych pól musimy je zwracać zamiast domyślnych wartości - wymaga edycji;
- Nie możemy obiektu przekazać gdzie indziej ze względu na powiązanie do modelu obiektowego SharePoint;
- Trzeba pamiętać o wykonaniu Update;
- Brak zarządzania stanem - możliwość dostępu do danych nie istniejących lub do danych już zmienionych.
Zalety
- Szybki i sprawny, silnie typowany dostęp do modelu obiektowego SharePoint;
- Silne typowanie;
- Dzięki interfejsowi, możliwe kilka implementacji;
- W porównaniu z brakiem mappowania, jedno miejsce dostępu (dzięki czemu wystarczy, że zmienimy kod w wrapperze);
- Pomoc kompilatora, jeżeli typ danych zwracanych jest różny - brak tej opcji w braku mappowania np.: var a = item[“name”], czym tutaj jest a?
Rozwiązanie
Ze względu na rozmiar projektu, nikt z nas nie chciał klepać tego kodu - przy jednej czy dwóch listach nie ma problemu, ale przy 50… co łącznie daje minimum 50*5=250 plików może to być trochę uciążliwe. Na szczęście z pomocą przyszedł nam CodeSmith Tools, dzięki któremu w dwa dni napisaliśmy sobie klasy generujące kod a potem jedynie konfigurację do naszych istniejących list.
Szablon, który użyliśmy załączam do postu – może komuś się przyda. Problem polega na tym iż był on pisany w lutym 2007 roku, a ostatnia jego aktualizacja była w lipcu 2007 roku i szczerze mówiąc nie pamiętam czy aby na pewno wszystkie typy pól SharePointa były w nim dobrze obsłużone :( Powyższy przykład został wygenerowany właśnie za pomocą tego narzędzia, więc na pewno działa (przynajmniej częściowo). Na rzecz tego postu trochę go zmodyfikowałem - usunąłem jeden poziom dostępu do danych, który istniał u nas jednak nie jest wymagany do poprawnego dostępu do danych. Także wykasowałem nazwę projektu z plików (na wszelki wypadek).
Rozwiązanie składa się z następujących plików:
- CommonScript.cs - udostępnia proste operacje na przestrzeni nazw oraz widoczności elementów;
- ProjCommonScript.cs - tworzy tablicę nazw pól, wykorzystywaną w pozostałych plikach;
- ProjIDataAccess.cst - szablon generujący interfejs dostępu do danych;
- ProjDataAccess.cst - szablon generujący implementację interfejsu dostępu do danych;
- ProjEntitiesConsts.cst - GUIDy dostępu do pól;
- ProjIEntities.cst - szablon generujący interfejs encji;
- ProjEntities.cst - szablon generujący implementację interfejsu encji;
- Proj.ClassGenerator.cst - szablon uruchamiający pozostałe szablony z przekazanymi ustawieniami generowania;
- Proj.CommonSettings.cst - definicja parametrów generowania szablonów;
- Gen_List_Settings.csp - przykładowe wartości parametrów z możliwością wygenerowania kodu.
Przykładowe parametry (sposób konfiguracji):
<?xml version="1.0"?>
<codeSmith xmlns="http://www.codesmithtools.com/schema/csp.xsd">
<propertySets>
<propertySet template="Proj.ClassGenerator.cst">
<property name="SharePointSite">http://URL TO SPS SITE</property>
<property name="SharePointWeb">/[URL TO WEB IF ANY]</property>
<property name="SharePointList">LIST NAME</property>
<property name="OutputDirectory">_GenOutput FOLDER</property>
<property name="ClassName">CLASS NAME</property>
<property name="Accessibility">Public</property>
<property name="IncludeNamespaces">True</property>
<property name="ModuleName">DONT REMEMBER</property>
<property name="ReturnAllItems">NAZWA WIELU ELEMENTOW NP Items</property>
<property name="ReturnSingleItem">NAZWA JEDNEGO ELEMENTU NP Item</property>
</propertySet>
</propertySets>
</codeSmith>
Po skonfigurowaniu pliku CSP, klikamy na niego prawym przyciskiem myszy i wybieramy opcję Generate Outputs:

UWAGA: Szablon udostępniony jest na zasadach „AS IS”, bez gwarancji, że będzie on działał. Jeżeli jednak zdecydujecie się na to podejście i będziecie mieć problem z szablonem, dajcie znać w miarę możliwości postaram się pomóc.
WrapperGenerator.zip (13.38 kb)