A while ago I stumbled upon a serious design limitation regarding Content Types and centralized document templates. What then followed was a series of testing, phone calls with Microsoft, finding alternative solutions and deep dive into Office Open XML.
Request from the customer
“We want to use MOSS 2007 to create a collaboration site per project for our 400+ projects. These collaboration sites all use the same Content Types and document templates. We want to centrally manage those document templates so that we don’t need to make the same change 400+ times.”
Approach
Due to sizing we architected a solution with a dozen of Site Collections that would each hold a collection of project sites. We ‘Feature’-ized our Content Types and Site Columns so that they could quickly be activated on all Site Collections and used by the child sites. Document templates would be stored in a central document library and we would link to them in the Content Types on the project sites.
First issue
Linking to document templates really doesn’t play well with the Document Information Panel (DIP). I have blogged about this here:
Centralizing Document Templates in a library- Document Information Panel shows incorrect properties
We proposed a solution where the document templates in the central library would be pushed to the Content Type resource folder on site level. The code to perform the push would have to connect to the Site Collections, copy the template to the resource folder (http://sitecollectionurl/_cts/contenttypename) and link template and Content Type together.
When a Site Content Type is associated with a List it will be a List Content Type inheriting from the Site Content Type and the document template will be copied to the List Content Type resource folder (http://sitecollectionurl/listurl/Forms/contenttypename).
Second issue
Did I tell you that the column values (metadata) have different values based on the project site ? So when a project site is created we automatically update the List Content Type Column default value with the values for that specific project site. Unfortunately this is not supported when working with Office 2007 file formats because they only react on changes to the Site Column.
Consider the following scenario:
1) Set up a document library with a Content Type that has a text column with a default value
2) Upload a new .doc or .docx as Content Type template
TEST 1) Create a new document:
.DOC: the DIP will contain the text column with the default value
.DOCX: the DIP will contain the text column with the default value
3) In SharePoint, modify the default value of the text column
TEST 2) Create a new document:
.DOC: the DIP will contain the text column with the updated default value
.DOCX: the DIP will contain the text column with the original default value
Microsoft confirmed that this is by design.
Third issue
When designing our document templates with Content Controls mapped to our SharePoint fields we didn’t know that internally in the DOCX file it uses a GUID for mapping the Content Control with the SharePoint Metadata XML. For fields (Site Columns) created in the UI or through API this is the SPWeb.ID of where they were created. For fields created declaratively through Features this is the SPList.ID of where their Content Type is associated to.
So some things to notice
- Creating a single document template with Content Controls mapped to your declaratively added Fields cannot be used in two different Document Libraries because the Content Controls lose the connection with Field (because the ID of the List is different and not updated in the Content Control)
- The solution here is to create your fields in the UI or through the API (this could be in a Feature Activating event)
- Copying a document template across Site Collections means different Web ID’s so it also affects fields created in the UI or the API
Finally
In the end we wrote some wrapper classes for Office 2007 file formats using System.IO.Packaging that would manipulate our document templates once they were copied over to a different Site Collection. We also rewrote our Features to create our Fields through the API (SPWeb.Fields.AddFieldAsXml()).
- Remove the SharePoint Metadata XML so that association of the Content Type to a List it would be regenerated automagically
- Loop through every Content Control and find to which Field they were mapped using information in it’s XPath. Then we would update the GUID’s in the Content Control to match the SPWeb.ID
Next time I’ll definitely take these design limitations into account. Lessons learned I’d say !
A refresher
The ListViewWebPart is used for displaying contents of a List or Document Library on both the default View pages (such as AllItems.aspx, etc) and also on other pages in the same web.
When a List View Web Part is added to a page (say the home page of that web) a hidden view (SPView) is created dedicated to that Web Part. When switching to another view in the Web Part Properties it really copies all the settings of the selected view into the dedicated hidden view. Changes made afterwards to the ‘selected view’ will not be pushed to the ‘dedicated view’.
Today’s question
How to programmatically modify the Toolbar settings for a ListViewWebPart ?
Answer
It all boils down to getting a reference to the hidden dedicated SPView and making modifications to it. There are three options that you can set the Toolbar to:
Full Toolbar
This option translates to “Standard” for the SPView.ToolbarType property.
Summary Toolbar
This option translates to “Freeform” for the SPView.ToolbarType property. Additionally you must specify a CAML string that is used for the rendering of the toolbar.
The default CAML string for a Links List looks like this:
|
]]>Add new link |
|
]]>
No Toolbar
This option translates to “None” for the SPView.ToolbarType property.
The code
I assume you are familiar with getting an instance to the ListViewWebPart and then retrieving the SPView instance using either reflection on the private SPView member or through the public Web and View GUID properties.
Next you can change the Toolbar schema XML through reflection as follows:
SPView view = ...;
Type viewType = view.GetType();
XmlNode toolbarNode = viewType.InvokeMember("GetNodeFromXmlDom", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, view, new object[] { "Toolbar" }) as XmlNode;
toolbarNode.Attributes["Type"].Value = "Standard";
view.Update();
Using xsd.exe you can generate a Class from your form schema (xsd) and then deserialize a form to an instance of that class. This makes it a lot easier to interact with its data.
The code for serialization and deserialization might look like this:
public static T Deserialize(Stream s)
{
T result = default(T);
XmlSerializer serializer = new XmlSerializer(typeof(T));
using (XmlTextReader reader = new XmlTextReader(s))
{
result = (T)serializer.Deserialize(reader);
}
s.Close();
return result;
}
public static T Deserialize(string s)
{
return Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(s)));
}
public static string Serialize(T o)
{
string result = null;
XmlSerializer serializer = new XmlSerializer(typeof(T));
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.Encoding = Encoding.UTF8;
using (MemoryStream stream = new MemoryStream())
{
using (XmlWriter writer = XmlTextWriter.Create(stream, settings))
{
serializer.Serialize(writer, o);
}
stream.Flush();
result = Encoding.UTF8.GetString(stream.ToArray());
}
return result;
}
However you lose the original processing instructions at the top of the XML file. If you want to keep those either do custom serialization using an XmlWriter or do some kind of merge code with the original XML and the XML coming from serialization.
Quite obvious if you think about it but I keep forgetting it :)
Today I visited a customer to solve an issue that I had run into a while ago in my post regarding ZIP file indexing with IFilters. The customer was indexing Office 2003 documents on a file share (.doc, .xls, .rtf, …) and had the issue of the Filename property having the strange value of fld and some numbers. This only occurred on their x64 live environment and not on a x86 test environment.
I looked at the IFilter overview using Citeknet IFilter Explorer (great tool !) and also the offfilt.dll (IFilter for the aforementioned file types) to check on version differences between the two systems but there were none.
Both environments were running SP2 and June 2009 Cumulative Update but since there aren’t that many obvious options I went for installing the November 09 Cumulative Update and that did the trick. Guess that the issues that was fixed for quite some time on x86 is only recently handled for x64 environments. Either way, everyone happy.
Hope it helps.