For the past several months, I've been experimenting with ways to use .NET reflection with the SharePoint API. As I've mentioned before, I believe that .NET attributes can not only make it easier to build great SharePoint applications, but can also provide a foundation for building better development tools.
In this series of posts about the evolving SharePoint Reflection Framework, I'll continue to share the results of my research and I'll include links to sample code directly within each post. As the code matures, I'll consider making it available on CodePlex so that others may contribute. For now, let's just treat it as a "thought experiment" as my friend Andrew Connell likes to call it.
The sample code and the framework assembly can be downloaded here:
http://www.johnholliday.net/downloads/JohnHolliday.SharePoint.ContentTypes.zip.
There is also a short screencast (27 minutes) that shows how to use the framework which you can view here:
Now let's take a closer look at how we can apply .NET attributes to simplify working with content types.
Some have asked, and you might also be wondering "Why do we need this? Why not just use CAML to declare the content types and then simply publish them using a feature? Why go to all the trouble of using this non-traditional approach?"
Well, there's nothing wrong with the traditional CAML-based approach. There is ample support for it and with a few simple tools, it's pretty straight-forward once you get the hang of it. But there are a number of limitations with the "code + markup" paradigm when it comes to reusability and extensibility. The markup is "brittle" in the sense that subtle mistakes are hard to find. True, you can preload the schema to help you remember what attributes go where, but there is no true intellisense and no strong typing to prevent those mistakes. If you want to extend an existing content type, you have to visit multiple files and switch back and forth between your event handler code and the markup. You have to spend a good deal of time making sure that the code matches the properties that have been defined in the markup.
The unfortunate reality is that working directly with XML still requires a significant paradigm shift in the way most developers think. With all the buzz about improving the SharePoint "developer experience", I keep thinking there must be a better way.
So, there are several goals for this experiment:
- We want to make it easier to declare content types in code.
- We want to eliminate the need to work directly in CAML or XML.
- We want to use the same coding idioms we are used to when working with other code.
- We want to make it easier to write custom event receivers for our content types.
- We want to make it easier to associate sub-components like document templates and XML documents with our content types.
- We want to be able to build libraries of reusable components that can be leveraged across multiple projects easily.
- We want to enable DRM for content type components at the assembly level so that developers can protect their intellectual property.
The SharePoint Reflection Framework
For this experiment, we'll construct a content type for an expense report in C#. But we also want to work with the expense report content type in the same way we might work with other C# classes. For instance, we might want to create a library of reusable content type classes for use in financial solutions.
To achieve this, we need to have a set of attributes that we can attach to a class to "declare" the content type and its properties.
We can start simply by deriving a ContentType class from System.Attribute. To control where and how it is used, we can mark it so that it can only be applied to classes and only once per class.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]public class ContentType : Attribute{ public string Name { get; set; } public string BaseType { get; set; } public string Description { get; set; } public string DocumentTemplate { get; set; } public string Group { get; set; } public bool Hidden { get; set; } public bool Sealed { get; set; } // ...}Within this attribute class, we have properties that specify the various well-known components of a content type, such as its name, group, base type, etc. These properties then show up as named properties of the attribute whenever it is applied to another class used to declare the content type itself. For our expense report experiment, that class might look something like this:
namespace JohnHolliday.SharePoint.SampleContentTypes{ [ContentType(Name = "Expense Report", BaseType = "Document", Description = "A worksheet for recording expenses.", Group = "SharePoint Reflection Framework Samples", Hidden = false] public class ExpenseReport { }}Notice that we are basically specifying the same properties we would have to add to a <ContentType> element in CAML. The difference is that here we get strong typing and intellisense support. Notice also that we don't have to worry about constructing the content type identifier. Instead, we just supply the base type and let the framework handle the details.
Next, within the content type declaration, we need to specify the fields that make up the type. Here again, .Net reflection comes to our aid to make things easier. What we really want is to declare the fields of the content type in the same way that we might declare properties for any C# class. So, if we wanted to add a description field to the expense report content type, we would naturally want to do something like this:
[ContentType(Name = "Expense Report", BaseType = "Document", Description = "A worksheet for recording expenses.", Group = "SharePoint Reflection Framework Samples", Hidden = false]public class ExpenseReport{ private string m_description=string.Empty; [FieldRef("Description")] public string Description { get { return m_description; } set { m_description=value; } }}Here, we can simply make use of a second attribute included in the framework to markup the properties we want to map to SharePoint fields. The FieldRef attribute is provided for this purpose. The framework supplies the necessary code to examine the ExpenseReport class, locate the mapped properties, determine their field types and link them automatically to the content type on creation. For instance, if we add a DateTime property to the ExpenseReport class, we want it to surface in SharePoint not as text, but as a date field. Similarly, the other field types can be automatically determined from the underlying property type.
Enumerations
We can extend this idea further to include built-in support for CHOICE fields if an enumerated type is mapped to a field. For example, we might need to declare a ProjectTypeEnum enumeration and then map it to a ProjectType property as shown below.
public enum ProjectTypeEnum{ Consulting, Training, Programming, Sales,}private ProjectTypeEnum m_projectType;[FieldRef("ProjectType", DisplayName = "Project Type", Required = true, Description = "The project type for which expenses are being submitted.", Group = "Sales")]public ProjectTypeEnum ProjectType{ get { return m_projectType; } set { m_projectType = value; }}
In this case, the framework detects that the underlying data type of the property is an enumerated type and it then generates the appropriate choices automatically, as shown here.
We can do a lot more with field references, but let's move on to the question of how to create the actual content type within the SharePoint environment.
Creating Instances
Typically, we'll be writing a FeatureActivated feature receiver or building a command-line utility, and we need to create a content type as part of the solution. Ideally, we'd like to end up with an instance of SPContentType that we can work with. So we really only need the framework to handle the dirty work of talking to the SharePoint API and then returning a properly constructed SPContentType object. One way to implement this pattern is to provide a factory method on the ContentType attribute class that takes a reference to our implementation class and then gives us back an SPContentType object. For the expense report content type, we might call it like this:
using (SPSite site = new SPSite("http://litwareinc.com")) { using (SPWeb web = site.OpenWeb()) { // Create the expense report content type. SPContentType ctExpenseReport = ContentType.Create(web, typeof(ExpenseReport)); if (ctExpenseReport != null) { // do something with it } }}Note that the ExpenseReport class is completely independent of the SPContentType object it was used to create. It can be implemented in a separate assembly. Using this approach, we can easily build libraries of content types that can be reused across multiple solutions. But there are still a couple of additional requirements we need to fulfill. Namely, how do we handle item event receivers? And what to do about the nested sub-components of a content type, such as document templates and XML documents? These are critical requirements, because we want to apply the same coding paradigm we are used to for other C# classes. We don't want to have to revert to using XML markup in order to specify event receivers and other elements.
Item Event Receivers
To specify event receivers for a content type in code, we need to add the appropriate entries to the EventReceivers collection of the SPContentType object. When working with XML markup, this is done by creating a special XML document that specifies which event we want to capture and the assembly and class in which the event receiver method is implemented.
To deal with event receivers in our ContentType attribute, we can take advantage of the fact that SharePoint provides an abstract class called SPItemEventReceiver that declares all of the event receiver methods. By simply deriving our ExpenseReport class from SPItemEventReceiver, we get the correct method declarations in the base class and then we can selectively override the ones we want. Instead of telling the framework explicitly which methods we are implementing as we would have to do with CAML, we can let the framework do the work for us and infer which methods have been implemented by examining the class via reflection. So all we have to do to handle the ItemAdded event, for example, is add the derivation and override the ItemAdded method as shown in the following code.
[ContentType(Name = "Expense Report", BaseType = "Document", Description = "A worksheet for recording expenses.", Group = "SharePoint Reflection Framework Samples", Hidden = false)]public class ExpenseReport : SPItemEventReceiver{ // ...properties public override void ItemAdded(SPItemEventProperties properties) { base.ItemAdded(properties); string message = string.Format("Expense report added to list: {0}", properties.ListTitle); EventLog.WriteEntry("SharePoint Reflection", message); }}Still, no CAML. We are working entirely within a C# class and we have captured all of the information needed to create the content type and to install an event receiver for the ItemAdded event. The framework looks at the ExpenseReport class and sees that only one of the abstract classes was implemented, so it creates an event receiver for that method only. It knows which assembly the class is declared in, so it automatically assigns the assembly and class names. If we want to add an event receiver for another event, such as ItemUpdated, we can simply add another override and implement the method. The framework automatically registers it for us.
Document Templates and XML Documents
The thing about document templates and other kinds of files that we might need to associate with a content type is that they are files. Ideally, we'd like to work with them as files without having to jump through hoops just to get SharePoint to recognize them. Fortunately, Visual Studio has a nice feature that lets us access and work with files easily from within our code. These are embedded resources. If we change the build action for any given file within our project to EmbeddedResource we can reference it using a path expression of the form <folder name>.<sub folder name>.<...>.<filename>.<extension>. So if we have a project folder named "DocumentTemplates" and a file in that folder named "ExpenseReport.xlsx", and both are part of a project named "SampleContentTypes", we could refer to the resource using the path "SampleContentTypes.DocumentTemplates.ExpenseReport.xlsx".
By extending the ContentType attribute to recognize and locate embedded resources, we can easily add document templates and other resources directly to our class declaration without having to worry about how they actually get copied into SharePoint. Here again, we can delegate that responsibility to the framework and just focus on the content type implementation.
For consistency and to distinguish document templates specified as embedded resources from those specified with legitimate urls, I've added a "res://" prefix which the framework recognizes. It then treats the url as a reference to an embedded resource and copies the file into the proper location when the content type is created. Since it already knows where the resource is located, we can drop the assembly name and just specify the assembly-relative path to the resource file. We end up with a declaration that looks like this:
[ContentType(Name = "Expense Report", BaseType = "Document", Description = "A worksheet for recording expenses.", Group = "SharePoint Reflection Framework Samples", Hidden = false, DocumentTemplate = "res://DocumentTemplates.ExpenseReport.xlsx")]public class ExpenseReport : SPItemEventReceiver{ //...}Future Directions
As you can see, there is a lot of promise to this approach, but we've only begun to scratch the surface. I'm currently working on one-way data binding of list items to properties in the implementation class whenever an event receiver method is called. This would essentially let you reference the mapped properties just like other properties in your event receiver methods. It would be one-way only because there might be times when you don't want those properties to get copied back into the list item automatically. For that, you can always call into the SharePoint API.
There is a lot more we can do with content types as well as with other SharePoint objects that can really make our lives easier as SharePoint developers. Give the code a spin and tell me what you think. In the coming weeks, look for additional posts that will go into more capabilities of the framework.
Stay tuned.