Creating a Custom Site Definition


A typical example of creating a custom site definition involves copying all the files from an existing definition to a new folder and modifying a couple XML documents. Though some similar concepts have been covered in previous chapters, the basic steps are included here.

Copying an Existing Site

Navigate to C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\1033, and you'll see several default site definitions. STS and MPS are at least two, and several have the SPS prefix. The STS folder contains a team site, a document workspace, and a blank site. The MPS folder contains the definitions for meeting workspaces.

To protect yourself from the benevolent updates that Microsoft releases, you cannot modify the shipping code. All you have to do to protect yourself is to copy one of those folders and save it with your own folder name. Creating one from scratch would take an eternity and would be prone to errors. Whenever a new site definition is needed, it's best to just copy one that exists and go from there. The STS has the team site, which is pretty straightforward, so we'll use that one as the foundation for our own.

We'll create a site called CPS, representing Custom Property Site, to demonstrate implementing the custom property. The next thing that needs to be done is to let SharePoint know this site exists.

Copying WEBTEMP.XML

When creating a new site from the user interface, SharePoint presents the user with several sites to choose from. These options are derived from the WEBTEMP.XML file and all WEBTEMP.XML files in the C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\1033\XML folder. Advanced developers will make friends with many files in this folder.

A simple cut and paste with a rename will get us a step closer to where we need to be. We can call our new WEBTEMP.XML file WEBTEMPCPS.XML.

Now that we have our duplicate file, we might assume that we could see the sites listed twice in the template selection page. Because the template names are the same, though, the site templates are not shown.

Customizing WEBTEMP.XML

Let's edit C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\1033\XML\WEBTEMPCPS.XML. The first thing we can do is to remove the MPS template node. The Name attribute of the Template node must match the folder name exactly. Within the WEBTEMPCPS.XML file, let's rename our template from STS to CPS. The Template node also has an ID field. Microsoft has indicated that the custom site definition should start at 10000 and must not conflict with any of the other WEBTEMP files. Set ours to 10000. The Title attribute is what the user who is creating the site will see in the select box. The imageUrl is what the user will see when he or she clicks the item. Description is always a lot of fun because it helps the user understand what that site template is designed for. These attributes are pretty straightforward, except for the ID attribute. The ID attribute relates to the Configurations section within the ONET.XML file. The ONET.XML file is one of those documents that you can't help but be familiar with if you are going to create your own custom site definitions, because each site definition has its own ONET.XML. So getting back to the Configuration node of the WEBTEMPCPS.XML document, let's just have one option for our Custom Property Site. We'll set the Hidden attribute to trUE for the nodes where the ID attributes have values 1 and 2. This is one of those few moments where you set something that does exactly what you think it should. Setting the Hidden attribute to TRue will cause SharePoint to suppress the option when displaying a list of site templates to choose from. Of course, deleting these two nodes would have had the same effect. Next, let's give our site a better name to avoid getting it confused with the "Team Site" that already exists in CPS. "Team Site - Custom Property" sounds great!

This is what your WEBTEMPCPS.XML document should look like.

Listing 4.1. WEBTEMPCPS.XML with New Template Added

 <?xml version="1.0" encoding="utf-8" ?> <!-- _lc _version="11.0.5510" _dal="1" --> <!-- _LocalBinding --> <Templates xmlns:ows="Microsoft SharePoint">   <Template Name="CPS" >     <Configuration  Title="Team Site - Custom Property"       Hidden="FALSE" ImageUrl="/_layouts/images/stsprev.png"       Description="This template creates a site for teams to       create, organize, and share information quickly and       easily. It includes a Document Library, and basic lists       such as Announcements, Events, Contacts, and Quick Links.">     </Configuration>     <Configuration  Title="Blank Site" Hidden="TRUE"       ImageUrl="/_layouts/images/stsprev.png" Description="This       template creates a Windows SharePoint Services-enabled Web       site with a blank home page. You can use a Windows SharePoint       Services-compatible Web page editor to add interactive lists       or any other Windows SharePoint Services features.">     </Configuration>     <Configuration  Title="Document Workspace" Hidden="TRUE"       ImageUrl="/_layouts/images/dwsprev.png" Description="This       template creates a site for colleagues to work together on       documents. It provides a document library for storing the       primary document and supporting files, a Task list for       assigning to-do items, and a Links list for resources related       to the document.">     </Configuration>   </Template> </Templates> 

Creating a Custom Document Library

Creating a custom document library is just about the same, only easier. It's nice to keep original stuff original and modify our own lists. Opening C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\1033\CPS\LISTS will reveal all the lists. Simply copy the DOCLIB from our CPS template C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\1033\CPS\LISTS and paste it in the same folder with the name DOCLIBCPS. This will be the document library that will use the custom property. Now we need to let SharePoint know this thing exists.

Modifying ONET.XML

Open ONET.XML located in C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\1033\CPS\XML for editing. In the ListTemplates node, you will find many ListTemplate elements. Duplicate the doclib node and call the new one doclibcps. The name must match the folder exactly. The Type must be less that 1000. Let's name ours 901.

Listing 4.2. ONET.XML Pointing to Our DOCLIBCPS

 <ListTemplate Name="doclib" DisplayName="Document Library"   Type="101" BaseType="1" OnQuickLaunch="TRUE" SecurityBits="11"   Description="Create a document library when you have a   collection of documents or other files that you want to share.   Document libraries support features such as sub-folders, file   versioning, and check-in/check-out."   Image="/_layouts/images/itdl.gif"   DocumentTemplate="101"></ListTemplate> <ListTemplate Name="doclibcps" DisplayName="Document Library   cps" Type="901" BaseType="1" OnQuickLaunch="TRUE"   SecurityBits="11" Description="Document Library with custom   property" Image="/_layouts/images/itdl.gif"   DocumentTemplate="101"></ListTemplate> 

Also in the ONET.XML file, one of the first nodes that we come across is the <PROJECT> node. Add an attribute to that called CustomJSUrl.

[View full width]

CustomJSUrl="/_layouts/[%=System.Threading.Thread.CurrentThread.CurrentUICulture.LCID%] /CPS/CustomJSLoader.aspx"


CustomJSUrl creates a custom JavaScript block with an SRC attribute that points where CustomJSUrl specifies. This script block is included on just about every page and can be used to perform whatever custom functionality you need. This is a very powerful feature, and we will be leveraging it wholeheartedly in this example. OWS.JS is shipping code and cannot be modified. You shouldn't even make a copy of it and use it in your own site definition. Instead, use CustomJSUrl.

Creating a New Site Based on a New Site Definition

Of course you must save your WEBTEMPCPS.XML and ONET.XML files, but that's not all. You also must reset IIS. This is easy enoughjust open the DOS prompt and execute IISReset. We are now ready to create a site! From just about any Create menu in SharePoint, you can choose to create new "Sites and Workspaces". Choose a name for the titleCPS sounds good for now, as shown in Figure 4.7. As a matter of fact, it sounds so good that we'll reuse it for the Web Site Address field, too.

Figure 4.7. Windows SharePoint site creation.


Now on to the Template Selection, where you should see your Team Site - Custom Property site template in the list of templates to choose from (see Figure 4.8).

Figure 4.8. Template selection.


If you don't, then something went wrong somewhere.

If SharePoint comes across two identical template names, it will ignore the second one. It's hard to say which one will be the second one. Another common mistake is not resetting IIS. IIS must be reset when any changes are made to WEBTEMP.XML or ONET.XML. Also ensure that other XML nodes weren't tampered with during our cut and paste adventures. Check the ID of the template when you receive messages that indicate that the template is invalid or cannot be found when trying to create the site. Remember we use all IDs over 10000.

Protecting the Files in the Layouts Folder

One last detail in an effort to fortify our application against Microsoft updates lies in the Layouts folder. The Layouts folder, C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\LAYOUTS, is mapped a virtual directory, _layouts, in all SharePoint sites. Any .NET page can run in this folder.

When you want to add some functionality quickly and easily, the Layouts folder can be your friend. This folder is where we can run our ASP.NET code. By simply adding a folder, we provide some great organization, and voila, you have your very own place to run ASP.NET. Keeping things neat in the Layouts folder provides maintainability and reduced time to market.

Sometimes a change to a page in the Layouts folder is so radical that you don't want any other SharePoint applications to know about it. Because all SharePoint sites share the same pages in the Layouts folder, it seems tempting to change one page and affect all applications. In either case, you shouldn't edit the original file in the Layouts folder. Instead, you should go through the exercise of creating your own Site Definition and pointing to a version of the pages in the Layouts folder.

When a user adds a new column to the document library, you might want to make your custom property available for him or her to choose. On the other hand, you might have a property that you don't want a user to add. It all depends on your needs. Remember to code to the requirementsalways develop what is needed rather than what is cool.

To make your custom property available to end users, there are a few pages that you can modify. Most of these pages are in the 1033 folder of the Layouts folder. 1033 represents English. If you have a multilingual application, then you will need to make these changes to each language you support. The only other file that you need to modify is in the document library itself.

If you were to navigate to ALLITEMS.ASPX of a document library and click Modify Settings and Columns, then you would be taken to LISTEDIT.ASPX in the 1033 folder. On the LISTEDIT.ASPX, there are two kinds of links that we are concerned with mostadd new column (FLDNEW.ASPX) and each column hyperlink in the list of columns (points to FLDEDIT.ASPX). FLDNEW.ASPX and FLDEDIT.ASPX are the two pages that we must edit.

The first thing we should do is create our own folder in the 1033 folder so that we don't mix our pages up with the Microsoft pages. This is a little more work than just making another page in the same folder, but it will make things clearer for the next person who must modify our application. Create a folder in the 1033 folder and call it CPS. Copy the LISTEDIT.ASPX, FLDNEW.ASPX, and FLDEDIT.ASPX files to the new CPS folder. Naturally, all our relative links are broken. Let's sum those up quickly.

FLDNEW.ASPX and FLDEDIT.ASPX are very straightforward. All that must be done to these pages is to repair the relative links. Only a couple link types break when you move these files: the JavaScript and style sheet references. This is easy enough to fix by putting ../ at the beginning of the path to each style sheet and JavaScript reference.

Listing 4.3. FldNew.aspx and FldEdit.aspx Snippets

 <script src="../ owsbrows.js"></script> <SharePoint:CssLink DefaultUrl="../ styles/ows.css" runat="server"/>     <SharePoint:Theme runat="server"/> <script><!-- if (browseris.mac && !browseris.ie5up) {     var ms_maccssfpfixup = "../ styles/owsmac.css";     document.write("<link rel='stylesheet' Type='text/css' href='" +     ms_maccssfpfixup + "'>"); } //--></script> <script src="../ ows.js"></script> 

The same fix needs to take place in the LISTEDIT.ASPX page, plus a couple more changes. Because LISTEDIT.ASPX generates several links that reference files located in the Layout folder, we must fix their relative paths as well. Find LSTSETNG.ASPX, ADVSETNG.ASPX (not used, but change it anyways), SAVETMPL.ASPX, SHROPT.ASPX, FORMEDIT.ASPX, VIEWEDIT.ASPX, and VIEWTYPE.ASPX and add ../ to the beginning of each. This will cause the browser to use the pages in the 1033 folder. Of course we could use brute force and just copy the entire Layout folder, but that's not a good practice. We only want to modify as little as possible. One last file that we must edit is the link that gets us to the LISTEDIT.ASPX in the first placeALLITEMS.ASPX.

ALLITEMS.ASPX resides in the Forms folder of the document library in question. ALLITEMS.ASPX and other dynamically created document library views don't actually contain the link to LISTEDIT.ASPX; instead it contains a toolbar that has the link. The toolbar is in SCHEMA.XML. In SCHEMA.XML, located in C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\1033\CPS\LISTS\DOCLIBCPS, find the Toolbar node with Type attribute set to RelatedTasksit's the last node in the XML document. Next, find the anchor tag for Modify Settings and Columnsit might be easiest just to use the "find" feature of your editing tool to look for LISTEDIT.ASPX and add /cps just before it. This will cause our custom LISTEDIT.ASPX page to open when a user clicks Modify Settings and Columns.

We've just completed the basic steps that are needed when creating a custom property for a document library. When a site is created based on your custom site definition, almost all the plumbing will be in place. We're not out of the woods yet. Leave these files open in your editor, and let's take a look at another reason why you might want to use a custom property so that we can more fully explore the solution.

Controls

We will need several controls to pull this off. The first and most obvious is the user interface that will provide the functionality we are a looking for. For our example, we need to display a list of authors from the pubs database, so we'll use a data access component to retrieve this and any other author information we need.

User Control

There are a couple reasons why a custom property must be implemented. One is for functionality, and the other is for the data that it provides. Whatever the situation, you'll probably use a user control for that functionality. The following code is used for providing our custom property:

Listing 4.4. AuthorLookup Class

 using System; using System.Data; using System.Drawing; using System.Web; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; using System.Collections; using SharePointBook; //namespace of our data access component,                       //defined in the next section namespace select{  public class AuthorLookup : System.Web.UI.UserControl {   //Global variables for public properties   private string _DefaultValue = "";   private string _ElemToSet = "idDefault";   private string _WhatToReturn = "value";   private string _Style = "";      private void Page_Load(object sender, System.EventArgs e) {}   protected override void Render(System.Web.UI.HtmlTextWriter writer){    Authors myExample = new Authors();    HtmlSelect mySelect = myExample.getAuthors();    mySelect.Style.Add("color", this.Style);    if (mySelect.Items.FindByValue(DefaultValue) != null){     mySelect.Items.FindByValue(DefaultValue).Selected = true;    }    switch (WhatToReturn.ToLower()){     case "value":      onChangeReturnValue(mySelect);      break;     case "both":      onChangeReturnBoth(mySelect);      break;     default:      onChangeReturnValue(mySelect);      break;    }    this.Controls.Add(mySelect);    base.Render (writer);   }   private void onChangeReturnValue(HtmlSelect mySelect)   {    mySelect.Attributes["onchange"] =     "document.getElementById('" + ElemToSet + "').value = "     + "this.options[this.selectedIndex].value;";   }   private void onChangeReturnBoth(HtmlSelect mySelect){    mySelect.Attributes["onchange"] =     "document.getElementById('" + ElemToSet + "').value = "     + "this.options[this.selectedIndex].value + '|'"     + "+ this.options[this.selectedIndex].text ;";   }   public string WhatToReturn   { get {return _WhatToReturn;} set {_WhatToReturn = value;} }   public string ElemToSet   { get {return _ElemToSet;} set {_ElemToSet = value;} }   public string DefaultValue   { get {return _DefaultValue;} set {_DefaultValue = value;} }   public string Style   { get {return _Style;} set {_Style = value;} }   #region Web Form Designer generated code   override protected void OnInit(EventArgs e) {    InitializeComponent();    base.OnInit(e);   }   private void InitializeComponent() {    this.Load += new System.EventHandler(this.Page_Load);   }   #endregion  } } 

Four public properties can be set on the user control. These properties help manage state and functionality.

The assumption is that we are using a select box for our foreign lookup. We've noticed that for select boxes, sometimes you just need the value, and sometimes you need both the value and the text. The WhatToReturn property determines whether the value of the selected item should be returned or both the value and the display. Here we use a | (a pipe character) to delineate between the value and the text.

The assumption with this control is that it will be used on a page in a modal window. The modal window will need to set the HTML element on the parent to a value. The ElemToSet property holds the ID of the element on the parent that needs to be populated.

The DefaultValue property holds the item that should be selected in the drop-down list.

The Style property is used in this example to set text to red. The significance of this example is to illustrate the idea that data is not the only thing that must be passed around but attribute information as well. Please don't limit yourself to attribute information. Other information or functionality can be passed back in the same way.

Example Data Access Component

The data access functionality has been broken out into a separate class to fetch the specific information we need. In this example we use the sqlHelper class of Microsoft.ApplicationBlocks.Data for our conduit into the SQL Server.

Listing 4.5. Author Data Access

 using System; using System.Data; using Microsoft.ApplicationBlocks.Data; using System.Data.SqlClient; using System.Web; using System.Web.UI.HtmlControls; namespace SharePointBook {  public class Authors  {   public Authors()   {   }   public HtmlSelect getAuthors()   {    string strConnection = "server=localhost\\wildwires;database=pubs;integrated"     + "security=SSPI";    System.Web.UI.HtmlControls.HtmlSelect select = new HtmlSelect();    select.ID ="myDefault";    select.DataSource = SqlHelper.ExecuteDataset(strConnection, "getAuthors", null);    select.DataTextField = "name";    select.DataValueField = "au_id";    select.DataBind();    select.Items.Insert(0,"");    return select;   }   public string GetAuthorByID(string AuthorId)   {    string returnValue = "No Record Found";    string strConnection = "server=localhost\\wildwires; database=pubs;integrated"     + "security=SSPI";    SqlParameter[] arParams = new SqlParameter[1];    arParams[0] = new SqlParameter("@AuthorID", AuthorId);    System.Data.SqlClient.SqlDataReader objDataReader =     SqlHelper.ExecuteReader(strConnection, "GetAuthorById", arParams);    while (objDataReader.Read() == true)    {     returnValue = objDataReader.GetValue(0).ToString();    }    return returnValue;   }  } } 

There are only two methods in this classGetAuthors and GetAuthorByID. GetAuthors returns all the authors through a stored procedure. In its current form, this function only stores the author's key, and that's not really what we want. We would like to display the author's name. This method can be used by a web service to return just the author's name.

Thinking about how the Style property is implemented, let's look at what it would take to get different lists from this same code. If we added a property to our user control, we could pass that value to a generic lookup. Perhaps there is a table that would have a list of queries and a key is passed in. Maybe pass a stored procedure name? This user control could even be made unfriendly by permitting the setting of the database connection information. Nobody would really want to provide users access to database configuration information, so we mentioned this example solely for mental calisthenics. Given a little thought, there is a lot of information that business users might want to store with the document.

Adding a New Column and Modifying Existing Columns in a Document Library Definition

When looking at FLDEDIT.ASPX and FLDNEW.ASPX, we can see that they have many things in common. They look almost identical from the user perspective, and they have just about the same code. In both cases, we have a list of properties to choose from. The user selects one type, and the options change for that type of field. Ultimately, we are shooting for a page that looks similar to Figure 4.9.

Figure 4.9. Adding a property to a document library.


When the user selects Red, the desired effect is to have the text of the select box turn redin Figure 4.10, the text Abraham Bennet would become red.

Figure 4.10. Optional settings for column.


First we'll cover what goes on behind the scenes, and then we'll walk through customized line by customized line of how to make this functionality happen. A few things are happening on this page. First and most straightforward is the list of field types and options. These are managed by conditionals. The property types are managed by if statements, and the optional settings portion is managed by a case statement. The other thing that happens on this page is a lot less obvious. When the user selects the various style settings, a round trip occurs through a JavaScript function called UpdatePage. When UpdatePage is fired, we need to get our custom setting to the user control. Let's begin by modifying FLDNEW.ASPX.

FLDNEW.ASPX

Starting from the top of the page and working our way down, the first thing we need to do is register the tag prefix for the user control.

Listing 4.6. FldNew.aspx Snippet

 <!-- _lc _version="11.0.5510" _dal="1" --> <!-- _LocalBinding --> <%@ Page language="C#" ... code removed to save space <!-- custom Code--> <%@ Register TagPrefix="uc1" TagName="AuthorLookup" src="/books/4/78/1/html/2/AuthorLookup.ascx" %> <!-- end custom code --> <% SPSite spServer = ... code removed to save space 

The next line of customization we come across is a variable declaration.

Listing 4.7. FldNew.aspx Snippet

 String strLookupList = ""; String strLookupField = ""; String strShowPresence = ""; // custom code string strALCustomSettings = ""; //end custom code String strDisplaySize = ""; String strDisplayNameParam = ""; int    iCurrencyLCID = spWeb.CurrencyLocaleID; 

The variable StrALCustomSettings represents the custom settings that need to be managed on the page locally. In our case, it will manage the Style setting that determines whether the text is red.

The next area helps us manage custom settings and default values in the querystring. This code isn't relevant until the page reloads. This reloading action is caused by the UpdatePage JavaScript function, which will be covered momentarily.

Listing 4.8. FldNew.aspx Snippet

 if ( Request.Url.ToString().IndexOf("LookupListParam") != -1 ) {strLookupList   = Request.QueryString.GetValues("LookupListParam")[0] ; } if ( Request.Url.ToString().IndexOf("LookupFieldParam") != -1 ) {strLookupField = Request.QueryString.GetValues("LookupFieldParam")[0]; } if ( Request.Url.ToString().IndexOf("ShowPresence") != -1 ) {strShowPresence = Request.QueryString.GetValues("ShowPresence")[0]; } //custom code if ( Request.Url.ToString().IndexOf("AuthorLookupParam") != -1 ) {strALCustomSettings = Request.QueryString.GetValues("AuthorLookupParam")[0] ; } if ( Request.Url.ToString().IndexOf("Default") != -1 ) {strDefault = Request.QueryString.GetValues("Default")[0] ; } //End Custom Code if (strFieldType == "") {     strFieldType = "Text"; } 

strDefault is a variable that is already part of FLDNEW.ASPX. We use it here for our custom property type.

The next two modifications are in the same JavaScript function, GetTypeDesc(). The reason behind this is to help make the application uniform and easily portable to other languages.

Listing 4.9. FldNew.aspx Snippet

 function GetTypeDesc(type) {  var L_TypeCounter_Text = "Counter";  ... code removed to save space.  var L_TypeComputed_Text = "Computed";  //custom code  var L_AuthorLookup_Text = "Author Lookup";  //end custom code  var L_TypeUnkown_Text = "Unknown type";  switch (type)  {   case "Text": return L_TypeSingleLine_Text;   ... code removed to save space.   case "URL": return L_TypeURL_Text;   //custom code   case "Author Lookup": return L_AuthorLookup_Text;   //end custom code   case "Computed": return L_TypeComputed_Text;  }  return L_TypeUnkown_Text; } 

The next JavaScript function is one that we have alluded to previously. The UpdatePage function is an event handler for the style radio buttons. The method ultimately forces a round trip by calling the document.location.replace method, replacing the URL with the same URL plus all the state information we need when the page reloads.

Listing 4.10. FldNew.aspx Snippet

 function UpdatePage(typeVal) {  var sArg = "&FieldTypeParam=";  var sFieldNameArg = "&DisplayNameParam=";  var sLookup = "Lookup";  // Custom Code  var sAuthorLookup = "AuthorLookup";  // end custom code  var sUser = "User";  var sLLArg = "&LookupListParam=";  var sLLVal = "";  //Custom Code  var sALArg = "&AuthorLookupParam=";  var sALVal = "";  var sALDArg = "&Default=";  var sALDVal = "";  //end custom code  var sPresArg = "&ShowPresence=";  var sPresVal = "";  var sURL = window.location + "";   ... code removed to save space.  if (typeVal == sLookup) {   sLLVal = GetLookupList();  if (sLLVal.length != 0)   sLLVal = sLLArg + sLLVal; } //Custom Code if (typeVal == sAuthorLookup) {   var tmpElems = document.getElementsByName("CustStyle");   if (tmpElems != "undefined") {    for(var i=0;i<tmpElems.length;i++) {     if (tmpElems[i].checked==true) {      sALVal = sALArg + tmpElems[i].value;     }    }   }   var tmpElem = document.getElementById("idDefault");   if (tmpElem != null){    sALDVal = sALDArg + document.getElementById("idDefault").value   }  }  //End Custom Code  if ((typeVal == sUser) || (typeVal == sLookup)) {   sPresVal = GetShowPresence();   if (sPresVal.length != 0)   sPresVal = sPresArg + sPresVal;  }  //Custom Code  var sNewUrl = sURL + sArg + typeVal + sFieldNameArg + nameval + sDescriptionArg + descval   + sLLVal + sPresVal + sALVal + sALDVal;  if (sNewUrl.length > 2040)   document.location.replace(sURL + sArg + typeVal + sLLVal + sALVal + sALDVal);  else   document.location.replace(sNewUrl); } 

The first added line of code creates a constant for the author lookupfairly simple. The next four lines are variables to help assemble the query string, specifically the custom settings and the default value. The if block checks to see whether the radio buttons exist and, if so, acquires the appropriate values and prepares them to be appended to the query string. Depending on the number of customizations you have, you might want to manage this differently. For example, if you offer 25 custom settings, it might not be wise to create a parameter for each. Instead, create a storage system to keep all the customization information in one parameter. Don't forget that you only get a little over 2,000 characters in the URL.

Finally, all the values are appended, and the page is reloaded. Another important thing to note here is that typeVal sets the property type. When UpdatePage is called, the property type is set to "Author-Lookup". A variable on the server side or ASP.NET inline code, strFieldType, is set by existing functionality. strFieldType is used for several things, including the "Optional Settings for Column" section, which will be covered later more in depth.

There is another function that behaves similarly to this one, called UpdateDateField(). It could be argued that there should be a function specific for custom fields, perhaps called UpdateCustomField(). It just seemed easier and more efficient to hijack UpdatePage(). Our example isn't so different that we couldn't use UpdatePage(), whereas the inner workings of UpdateDateField() are quite a bit different. The point is that if a custom field is more complicated, it might be wise to create a different handler for it.

There are no custom properties in SharePoint, but you can create a text type property and put your own user interface around it. This will accomplish several thingsit will provide custom functionality, custom data, or both. The field schema is XML that describes a property. Every property (or field) in document libraries has a field schema. The next thing we will do is to modify the field schema at the time that it is created.

Listing 4.11. FldNew.aspx snippet

 if (GridWidth < 2)  {   alert(L_alert20_Text);   frm.GridNumRange.focus();   return false;  } } //custom code if (Type == "AuthorLookup") {  Type = "Text";  Format = "AuthorLookup"; } //end custom code var Schema = ('<Field ' +  (Name         ? 'Name="'        + SimpleHTMLEncode(Name)         + '" ' : '') +  (FromBaseType ? 'FromBaseType="'+ SimpleHTMLEncode(FromBaseType) + '" ' : '') +  (DisplaySize  ? 'DisplaySize="' + DisplaySize                    + '" ' : '') +   ... code removed to save space.  (Description  ? 'Description="' + SimpleHTMLEncode(Description)  + '" ' : '') +  (Required     ? 'Required="'    + SimpleHTMLEncode(Required)     + '" ' : '') +  (NumLines     ? 'NumLines="'    + SimpleHTMLEncode(NumLines)     + '" ' : '') +  (Format       ? 'Format="'      + SimpleHTMLEncode(Format)       + '" ' : '') +  (MaxLength    ? 'MaxLength="'   + MaxLength                      + '" ' : '') +  (Min          ? 'Min="'         + Min                            + '" ' : '') +  (Max          ? 'Max="'         + Max                            + '" ' : '') + 

Just before the field schema gets defined, the Type is set back to Text, and Format is hijacked and set to "AuthorLookup". This little ballet keeps SharePoint happy and lets us know what type of field we are working with later on. Any time there is a text field, the format needs to be checked to see whether it's one of the custom types, and if so, it must be rendered accordingly. This model is similar to the one that Microsoft uses for checkboxes and radio buttons.

The next addition is for storing our custom settings. We only have one custom setting: whether or not the text field is red.

Listing 4.12. FldNew.aspx Snippet

 }else if(Default) {  Schema += '<Default>' + SimpleHTMLEncode(Default) + '</Default>'; } //Custom Code if (Type == "Text" && Format=="AuthorLookup") {  Schema += '<CustomSettings><Style><%=strALCustomSettings%></Style> </CustomSettings>'; } //End Custom Code if (Type == "Calculated") {     if (!CheckForIllegals(Formula)) 

The Default node and the CustomSettings node become child nodes of the Field element. Anything you can encode as XML can be stored here. The business requirement might call for a very complicated interface. The interface can be designed by the user, and the settings for that field are stored in the field schema.

This is an opportunity for self-exploration into field schemas because this is where they are created. It's outside the scope of a custom property, but take a look at how the Calculated fields are handled and the different choice options. The big take-away here is to understand that we store the custom settings for your field as an XML document in the child node of the Fields element.

After all that, we can finally get down to the user interface modifications. The first thing to do is to create the radio button for the custom property type.

Listing 4.13. FldNew.aspx Snippet

[View full width]

 <TR>  <TD align="center" nowrap >  <% if ( strFieldType == "Calculated" ) { %> ... Code removed to save space  </TD>  <TD  ... Code removed to save space  </TD> </TR> <!-- Custom Code --> <TR>  <TD align="center" nowrap >  <% if ( strFieldType == "AuthorLookup" ) { %> <INPUT onClick="UpdatePage('AuthorLookup')"  type="radio" value="AuthorLookup" id=onetidTypeCalculated name="Type" title="The type of  information in this column is : Author Lookup" CHECKED> <% ; } else { %> <INPUT  onClick="UpdatePage('AuthorLookup')" type="radio" value="AuthorLookup"  id=onetidTypeCalculated name="Type" title="The type of information in this column is :  Author Lookup" > <% ; } %>  </TD>  <TD  ><LABEL FOR="onetidTypeAuthorLookup"> Author Lookup<!-- -->< /LABEL></TD> </TR> <!-- end custom code --> <TR>  <TD align="center" nowrap ></TD>  <TD  ID=align101></TD> </TR> 

The easiest way to do this is to copy the bold line (the table row for Calculated) and change a few things. First, search for Calculated and replace it with AuthorLookup. The Title attribute also needs to be changed to reflect AuthorLookup, and finally the parameters that are sent to UpdatePage() need to be AuthorLookup. There are two HTML elements that are conditionally displayed, so be sure to modify them both. Now, on to the option for the property.

Options

The options are displayed a little differently in that they are done with a case statement on the server side. Using cut and paste, it's simple to copy an option that exists and modify it based on the business needs.

Listing 4.14. FldNew.aspx Snippet

 <% break; // custom code case "AuthorLookup": %>  <!-- AuthorLookup -->  <TR>   <TD colspan=2></TD>   <TD  width=10>&nbsp;</TD>   <TD      id=onetidTypeDefaultAuthorLookupValue><label for="idDefault">Default     value</label>:<FONT size=3>&nbsp;</FONT><BR>    <TABLE border=0 cellspacing=1>     <TR>      <TD >       <!-- custom code -->       <input type=hidden name="Default"         value=<%SPEncode.WriteHtmlEncodeWithQuote(Response, strDefault,        '"');%>>       <%       idALookup.DefaultValue = strDefault;       idALookup.Style = strALCustomSettings;       %>       <uc1:AuthorLookup name="ALookup"         runat="server"></uc1:AuthorLookup>      </TD>     </TR>     <!-- custom code -->     <TR>      <TD >       <label for="idDefault">Style:</label><BR>       <input type="radio" name="CustStyle" value="normal"         onclick="UpdatePage('AuthorLookup')"        checked><label for="idCustStyleNormal">Normal</label></input><BR>       <input type="radio" name="CustStyle" value="red"         onclick="UpdatePage('AuthorLookup')"><label        for="idCustStyleRed">Red</label></input>       <script>        tmpElems = document.getElementsByName("CustStyle")        for(var i=0;i<tmpElems.length;i++) {         if (tmpElems[i].value=="<%=strALCustomSettings%>") {          tmpElems[i].checked = true;          break;         }        }       </script>      </TD>     </TR>    <!-- end custom code -->    </TABLE>   </TD>  </TR> <% break; case "URL": %>  <!-- URL -->  <TR> 

The entire case section has been added. Again, you can copy a different node and modify it until the desired results have been achieved. The pieces that will be detailed further are bold. The rest is pretty straightforward.

 <% idALookup.DefaultValue = strDefault; idALookup.Style = strALCustomSettings; %> <uc1:AuthorLookup name="ALookup"        runat="server"></uc1:AuthorLookup> 


The user control, for which the code was introduced earlier, is where our custom functionality originates. Here a couple pieces of information are passed to the control. strDefault has already been managed for us by existing code on the page. strALCustomSettings represents the custom settings that need to be passed to the control. This is too important to just let slip byunderstand that whatever custom functionality your control can provide can be passed in this manner. Each option doesn't have to be defined explicitly. The raw data from whatever state management system you come up with can be passed straight through to the user control. The user control is then responsible for referencing your state management system and render accordingly. This concept is intentionally left simple. The complexity should only be driven by the business problems, and the solution should be kept as simple as possible.

That's it for FLDNEW.ASPXyou should now be able to view this page in your browser and create a new property of type author lookup. Now onto FLDEDIT.ASPX, but first let's take a look at some things we'll need.

FLDNEW.ASPX and FLDEDIT.ASPX are just about identical. FLDEDIT.ASPX is a little more complicated than FLDNEW.ASPX simply because FLDEDIT.ASPX must manage existing state. We decided earlier that whenever we come across a text field, we need to look in the Format property to see whether it is one of our custom types. To accomplish this, we need to introduce two foreign elements into the mixSPCustomField and FLDTYPESCUSTOM.XML. Neither exists natively in SharePoint.

FLDTYPESCUSTOM.XML

FLDTYPES.XML is considered shipping code, and it shouldn't be modified if you hope to survive a service pack update or hot fix. The neat thing about FLDTYPES.XML is that it contains the definition for all properties. It's nice to understand what goes on in this file, just so that it's easier to figure out solutions to other problems. What must be done in our case is to identify a custom field and what type it is. Following the same model that Microsoft uses, we too will use an XML document to store our custom types. So it only makes sense to create a file called FLDTYPESCUSTOM.XML. This is nothing more than a copy of FLDTYPES.XML (C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\1033\XML) with many things removed. The only nodes we keep are MetaData and the data\rows\row model. The only reason to keep the MetaData element is in case some time in the future you would rather work with the rendering patterns in the same way Microsoft does.

Listing 4.15. FLDTYPESCUSTOM.XML

 <List>  <MetaData>   <Fields>    <Field Type="Text" Name="TypeName" DisplayName="TypeName" />    <Field Type="Text" Name="InternalType" DisplayName="InternalType" />    <Field Type="Text" Name="Sortable" DisplayName="Sortable" />    <Field Type="Text" Name="Filterable" DisplayName="Filterable" />    <Field Type="Text" Name="SQLType" DisplayName="SQLType" />    <Field Type="Text" Name="SQLType2" DisplayName="SQLType2" />    <RenderPattern Type="Note" Tall="TRUE" Name="HeaderPattern"      DisplayName="HeaderPattern" />    <RenderPattern Type="Note" Tall="TRUE" Name="DisplayPattern"      DisplayName="DisplayPattern" />    <RenderPattern Type="Note" Tall="TRUE" Name="EditPattern"      DisplayName="EditPattern" />    <RenderPattern Type="Note" Tall="TRUE" Name="NewPattern"      DisplayName="NewPattern" />    <RenderPattern Type="Note" Tall="TRUE"      Name="PreviewDisplayPattern"      DisplayName="PreviewDisplayPattern" />    <RenderPattern Type="Note" Tall="TRUE" Name="PreviewEditPattern"      DisplayName="PreviewEditPattern" />    <RenderPattern Type="Note" Tall="TRUE" Name="PreviewNewPattern"      DisplayName="PreviewNewPattern" />    <RenderPattern Type="Note" Tall="TRUE" Name="HeaderBidiPattern"      DisplayName="HeaderBidiPattern" />    <RenderPattern Type="Note" Tall="TRUE" Name="DisplayBidiPattern"      DisplayName="DisplayBidiPattern" />    <RenderPattern Type="Note" Tall="TRUE" Name="EditBidiPattern"      DisplayName="EditBidiPattern" />    <RenderPattern Type="Note" Tall="TRUE" Name="NewBidiPattern"      DisplayName="NewBidiPattern" />   </Fields>  </MetaData>  <Data>   <Rows>    <Row>     <Field Name="TypeName" DisplayName="TypeName">AuthorLookup</Field>    </Row>   </Rows>  </Data> </List> 

For each custom field, simply add another <Row/> element.

SPFieldCustom

Every other property type in SharePoint is represented by a class. We need, at minimum, a helper class to identify whether or not the field is custom and to gather information about it. The assembly is designed to run at C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\LAYOUTS\BIN.

Listing 4.16. SPCustomField Helper Class

 using System; using System.Xml; using System.Xml.XPath; using Microsoft.SharePoint; namespace SharePointBook {  public class SPCustomField  {   public SPCustomField() { }  public bool isCustomField(SPField spField)  {   string lcid = spField.ParentList.ParentWeb.Locale.LCID.ToString();   string strPath = System.Reflection.Assembly.GetExecutingAssembly().CodeBase;   int idxMyString = strPath.IndexOf("layouts");   strPath = strPath.Remove(idxMyString, strPath.Length-idxMyString);   strPath = strPath + lcid + "/XML/FLDTYPESCUSTOM.xml";   XmlDocument xmlDocument = new XmlDocument();   xmlDocument.Load(strPath);   string strXPath = "//Data/Rows/Row/Field[@Name='TypeName'][. = '"    + GetAttributeValue(spField, "Format") +"']";   XmlNode xmlNode = xmlDocument.SelectSingleNode(strXPath);   return (xmlNode != null);  }  public string GetAttributeValue(SPField spField, string attribute)  {   string strDocument = spField.SchemaXml;   string strResult = "";   XmlDocument xmlDocument = new XmlDocument();   xmlDocument.LoadXml(strDocument);   XmlNode xmlNode = xmlDocument.SelectSingleNode("//Field[@DisplayName='"     + spField.Title + "']");   XmlAttribute xmlAttribute = xmlNode.Attributes[attribute];   if (xmlAttribute != null){    strResult = xmlAttribute.Value;   }   return strResult;  }  public string GetCustomSetting(SPField spField, string name) {    string strDocument = spField.SchemaXml;    string strResult = "";    XmlDocument xmlDocument = new XmlDocument();    xmlDocument.LoadXml(strDocument);    XmlNode xmlNode = xmlDocument.SelectSingleNode ("//CustomSettings/" + name);    if (xmlNode !=null){     strResult = xmlNode.InnerText;    }    return strResult;   }  } } 

As you can plainly see, no error handling appears in this code; it was written just to explain these points and was kept as simple as possible. There are three methods in this class. The isCustomField takes in an SPField object and determines whether it's a custom type by looking for the Format value in our FLDTYPESCUSTOM.XML file. The GetAttributeValue is probably misleading because it only gets the attributes on the field node, not the children. Finally, we have the GetCustomSettings method, which returns the inner text of a child node of the CustomSettings node.

Your customization needs will drive your storage and retrieval models. This example assumes that everything will be stored in child nodes. It might, however, turn out that child nodes with attributes are required.

Now that we have a way to determine which fields are custom and which are not, we can dig into FLDEDIT.ASPX.

FLDEDIT.ASPX

The fundamental difference between FLDEDIT.ASPX and FLDNEW.ASPX is that FLDEDIT.ASPX must entertain a preexisting condition. It's a minor difference, and the modifications are generally the same, though some additional code is required for FLDEDIT.ASPX. Instead of trying to compare and contrast the two files, we'll begin from the top on FLDEDIT.ASPX just as we did with FLDNEW.ASPX.

First, we need to register our user control at the top of the page.

Listing 4.17. FldEdit.aspx Snippet

 <!-- _lc _version="11.0.5510" _dal="1" --> <!-- _LocalBinding --> <%@ Page language="C#"  ... code removed to save space <!-- custom Code--> <%@ Register TagPrefix="uc1" TagName="AuthorLookup" src="/books/4/78/1/html/2/AuthorLookup.ascx" %> <!-- end custom code --> <%     String strType = ""; 

Near the top of the page, we need to declare a global variable that will represent our SPCustomField class.

Listing 4.18. FldEdit.aspx Snippet

 <% //Custom Code SharePointBook.SPCustomField spCustomField = newSharePointBook.SPCustomField(); //End Custom Code SPListCollection spLists = spWeb.Lists; SPList spList = spLists.GetList(new Guid(Request.QueryString.GetValues("List")[0]), true); %> 

The spCustomField object will be used later to determine whether a text field is actually a custom field and to get the custom type and custom settings. Next, strALCustomSettings needs to be declared to keep track of the custom settings that the user requests.

Listing 4.19. FldEdit.aspx Snippet

 String strLookupList = ""; String strLookupField = ""; String strShowPresence = ""; // custom code string strALCustomSettings = ""; //end custom code String strDisplaySize = ""; String strDisplayNameParam = ""; int    iCurrencyLCID = spWeb.CurrencyLocaleID; 

strALCustomSettings will ultimately be used to store the custom settings on the server side so that they can be used by the user control. The next modification actually populates that value along with the default value. The assumption is that the query string information is most current. The query string is populated from a post back from UpdatePage().

Listing 4.20. FldEdit.aspx Snippet

 if ( Request.Url.ToString().IndexOf("LookupFieldParam") != -1 ) {strLookupField = Request.QueryString.GetValues("LookupFieldParam")[0]; } if ( Request.Url.ToString().IndexOf("ShowPresence") != -1 ) {strShowPresence = Request.QueryString.GetValues("ShowPresence")[0]; } //Custom Code if ( Request.Url.ToString().IndexOf("Default") != -1 ) {strDefault = Request.QueryString.GetValues("Default")[0] ; } else{strDefault = spField.DefaultValue;} if ( Request.Url.ToString().IndexOf("AuthorLookupParam") != -1 ) {strALCustomSettings = Request.QueryString.GetValues("AuthorLookupParam")[0] ; } else{strALCustomSettings = spCustomField.GetCustomSetting(spField,"Style");} //end custom code if (strFieldType == "") {  strFieldType = "Text"; } if (spField.TypeAsString == strFieldType ||  (spField.Type == SPFieldType.MultiChoice && strFieldType == "Choice")) {  strDefaultFormula= spField.DefaultFormula;  if (strDefaultFormula == null ||   strDefaultFormula == String.Empty)  {   //Custom Code   if (strFieldTypeParam == ""){    strDefault = spField.DefaultValue;   }   //End Custom Code  }  switch(spField.Type)  {   case SPFieldType.Text:   {  //Custom Code  if (spCustomField.isCustomField(spField))  {   if (strFieldTypeParam == ""){    strFieldType = spCustomField.GetAttributeValue(spField, "Format");   }  }  else  {   //Existing code   SPFieldText field = (SPFieldText)spField;   iMaxLength = field.MaxLength;   //End existing code  }  //End Custom Code  break; } case SPFieldType.Calculated: 

The goal of this long block is to set the default value and the custom settings value. If there is query string information, it should be used.

Next, we look at the UpdatePage() method, which causes the post back when the user selects a property type or modifies the configuration information.

Listing 4.21. FldEdit.aspx Snippet

 function UpdatePage(typeVal) {  var sArg = "&FieldTypeParam=";  var sFieldNameArg = "&DisplayNameParam=";  var sLookup = "Lookup";  // Custom Code  var sAuthorLookup = "AuthorLookup";  // end custom code  var sUser = "User";  var sLLArg = "&LookupListParam=";  var sLLVal = "";  //Custom Code  var sALArg = "&AuthorLookupParam=";  var sALVal = "";  var sALDArg = "&Default=";  var sALDVal = "";  //end custom code  var sPresArg = "&ShowPresence=";  var sPresVal = "";  var sURL = window.location + "";  var nameval = escapeProperly(document.frmFieldData.DisplayName.value);  var sDescriptionArg = "&DescriptionParam=";  var descval = escapeProperly(document.frmFieldData.Description.value);  var dwArgPos = sURL.indexOf(sArg);  if (dwArgPos!=-1) {   sURL = sURL.substr(0,dwArgPos);  }  if (typeVal == sLookup) {   sLLVal = GetLookupList();   if (sLLVal.length != 0)    sLLVal = sLLArg + sLLVal;  }  //Custom Code  if (typeVal == sAuthorLookup) {   var tmpElems = document.getElementsByName("CustStyle");   if (tmpElems != "undefined") {    for(var i=0;i<tmpElems.length;i++) {     if (tmpElems[i].checked==true) {      sALVal = sALArg + tmpElems[i].value;     }    }   }   var tmpElem = document.getElementById("idDefault");   if (tmpElem != null){    sALDVal = sALDArg + document.getElementById("idDefault").value   } }  //End Custom Code  if ((typeVal == sUser) || (typeVal == sLookup)) {   sPresVal = GetShowPresence();   if (sPresVal.length != 0)    sPresVal = sPresArg + sPresVal;  }  var sNewUrl = sURL + sArg + typeVal + sFieldNameArg + nameval + sDescriptionArg + descval   + sLLVal + sPresVal + sALVal + sALDVal;  if (sNewUrl.length > 2040)   document.location.replace(sURL + sArg + typeVal + sLLVal + sALVal + sALDVal);  else   document.location.replace(sNewUrl); } 

First, a variable is declared and set to AuthorLookup. This variable will be used later to determine what Type of property we are dealing with. Next, several variables are initialized to manage the query string information.

Right now there is only one parameter for the custom settings. Given a particular problem, it might be acceptable to have more than one or to keep all values in one delimited string. XML documents sound nice, but they take up a lot of space on the precious query string. Other state management techniques include viewstate, cookies, and session variables to name a few.

A quick note about session variablesfirst, they must be turned on. Second, all unghosted pages in the site are forced to participate in the session. Normally, if an ASP.NET page doesn't use a session variable, it doesn't have to participate in the session. If there are several unghosted pages on your site, this could cause a huge performance hit. Unghosted pages are those that are stored in the database. When a page is modified in FrontPage 2003 and saved, it is saved to the database and becomes unghosted. It is then processed through the SafeMode parser, which behaves differently in SharePoint than in normal ASP.NET.

The next section of code determines what variables are checked. If this were a little more complicated, or even in this case, it could be argued that we should break out this code or create a new method handler instead of using the already existing one. There are some benefits to this idea, namely that the state management would be easier to deal with and much cleaner.

When looking at UpdatePage(), also take a look at UpdateDateField() and how it works. This should spark some ideas and help you to come up with a solution that works best for the business problem at hand.

After all the variables are set, they are appended and ultimately sent to the location property for the browser.

Just as before, at the last moment we want to set the Type to Text and set the Format to AuthorLookup.

Listing 4.22. FldEdit.aspx Snippet

 //Custom Code  if (Type == "AuthorLookup")  {   Type = "Text";   Format = "AuthorLookup";  }  //End Custom Code  var Schema = ('<Field ' +   (Name      ? 'Name="'     + SimpleHTMLEncode(Name)      + '" ' : '') +   (FromBaseType ? 'FromBaseType="'+ SimpleHTMLEncode(FromBaseType) + '" ' : '') + 

This section of code sets the field schema. The field schema is XML that describes a property in the document library. The following code sets the default value and the custom settings. Take a close look at the custom settings.

Listing 4.23. FldEdit.aspx Snippet

 }else if(Default)  {   Schema += '<Default>' + SimpleHTMLEncode(Default) + '</Default>';  }  //Custom Code  if (Type == "Text" && Format == "AuthorLookup")  {   Schema += '<CustomSettings><Style><%=strALCustomSettings%></Style></CustomSettings>';  }  //End Custom Code  if (Type == "Calculated")  {   if (!CheckForIllegals(Formula))      return false;  if (Formula.charAt(0) != '=')  {     Formula = "="+Formula;  }  Schema += '<Formula>' + SimpleHTMLEncode(Formula) +'</Formula>'; } 

The custom settings are a simple bit of XML. Given the situation, this section could become much more complex. The custom settings section is used to store the customized information about your user control.

The user interface is somewhat different from FLDNEW.ASPX. Some types can be converted to others because of the way the data is stored. We have built the custom properties so that they will be interchangeable with anything that a Text type is interchangeable with.

Listing 4.24. FldEdit.aspx Snippet

[View full width]

 if (spField.Type == SPFieldType.Number ||   spField.Type == SPFieldType.Currency ||   spField.Type == SPFieldType.Boolean) { %>  <TR>   <TD align="center" nowrap >   <% if ( strFieldType == "Boolean" ) { %> ... Code removed to save space   </TD>   <TD  ... Code removed to save space ...</TD>  </TR> <% } //Custom Code if (spField.Type == SPFieldType.Text ||   spField.Type == SPFieldType.Choice ||   spField.Type == SPFieldType.Note ||   spField.Type == SPFieldType.MultiChoice ||   spField.Type == SPFieldType.DateTime) { %>  <TR>   <TD align="center" nowrap >   <% if ( strFieldType == "AuthorLookup" ) { %> <INPUT onClick="UpdatePage ('AuthorLookup')" type="radio" value="AuthorLookup" id=onetidTypeCalculated name="Type"  title="The type of information in this column is : Author Lookup" CHECKED> <% ; } else { %>  <INPUT onClick="UpdatePage('AuthorLookup')" type="radio" value="AuthorLookup"  id=onetidTypeAuthorLookup name="Type" title="The type of information in this column is :  Author Lookup" > <% ; } %>   </TD>   <TD  ><LABEL  FOR="onetidTypeAuthorLookup">Author Lookup<!-- --> </LABEL></TD>  </TR> <% }//End Custom Code %>  <TR>   <TD align="center" nowrap ></TD>   <TD  ID=align101></TD>  </TR> </TABLE> 

If you look at the preceding snippet of code in context, you can see how the various properties are rendered. The code in bold is our author lookup radio button. When creating a new custom property type, it is easiest just to copy and paste a new row from an existing row. Modify the new section of code to handle your property type.

Listing 4.25. FldEdit.aspx Snippet

[View full width]

 <% break; //Custom Code case "AuthorLookup": %>  <!-- AuthorLookup -->  <TR>   <TD colspan=2></TD>   <TD  width=10>&nbsp;</TD>   <TD  id=onetidTypeDefaultAuthorLookupValue><label  for="idDefault">Default value</label>:<FONT size=3>&nbsp;</FONT><BR>    <TABLE border=0 cellspacing=1>     <TR>      <TD >       <input type=hidden name="Default"  value=<%SPEncode .WriteHtmlEncodeWithQuote(Response, strDefault, '"');%>>       <%       idALookup.DefaultValue = strDefault;       idALookup.Style = strALCustomSettings;       %>       <uc1:AuthorLookup name="ALookup"  runat="server"/>      </TD>     </TR>     <!-- custom code -->     <TR>      <TD >       <label for="idDefault">Style:</label><BR>       <input type="radio" name="CustStyle" value="normal"   onclick="UpdatePage('AuthorLookup')" checked><label for="idCustStyleNormal">Normal</label>< /input><BR>       <input type="radio" name="CustStyle" value="red"   onclick="UpdatePage('AuthorLookup')"><label for="idCustStyleRed">Red</label></input>       <script>        tmpElems = document.getElementsByName("CustStyle")        for(var i=0;i<tmpElems.length;i++) {         if (tmpElems[i].value=="<%=strALCustomSettings%>") {          tmpElems[i].checked = true;          break;         }        }       </script>      </TD>     </TR>     <!-- end custom code -->    </TABLE>   </TD>  </TR> <% break; //end custom code case "URL": %>  <!-- URL -->  <TR>   <TD colspan=2></TD> 

An entire new case was added to handle the options for AuthorLookup type. The items in bold are what's really different between other rows. As a developer, you can gain a lot of flexibility from this technique. User controls can be added as well as plain HTML elements. Think of this as a normal web part page.

Now the user can edit a field that he or she has previously created.

LISTEDIT.ASPX

Revisiting LISTEDIT.ASPX, we can see that there is a column of type Text, which should be the Author Lookup. The SPCustomField classGetAttributeValue method can be used to fix this problem. First, declare the object at the top of the page.

Listing 4.26. Listedit.aspx Snippet

 SharePointBook.SPCustomField spCustomField = newSharePointBook.SPCustomField(); string[] strLists = Request.QueryString.GetValues("List"); 

The next step is to find out what kind of custom property this is; remember that there can be more than one.

Listing 4.27. Listedit.aspx Snippet

 switch (spField.Type) {  case SPFieldType.Text:   //Custom Code   switch (spCustomField.GetAttributeValue(spField,"Format"))  {   case "AuthorLookup": %>   <TD class=ms-propertysheet>Author Lookup</TD> <%   break;   default: %>   <TD class=ms-propertysheet>Single line of text</TD> <%   break;   }   //End Custom Code  break;  case SPFieldType.Note: 

This will fix the display problems and indicate to the user the correct property type. Even though it's really a text type, we can use the Format attribute to override the single line of text functionality and present something else.

Modifying the FLDNEW.ASPX, FLDEDIT.ASPX, and LISTEDIT.ASPX files and adding a few support features is all that is needed to create and maintain a custom property type. But that doesn't do much good if you can't use it. Incorporating a custom property when a user uploads or modifies a document through the web browser or Microsoft Word is the next step in this process.

Uploading and Modifying a Document in the Web Interface and in Microsoft Word

When a user modifies properties through the web, he or she will see an interface that looks like Figure 4.11.

Figure 4.11. Modifying properties of a document.


"Test5" is the author lookup field and is represented as a single line of text. To understand how to modify this field, it's important to understand how the fields are rendered.

How Fields Are Rendered

Before taking the jump to entering or modifying properties, we need to take a closer look at how the HTML is rendered on the page. Viewing the HTML source and searching for one of your fields will reveal a script block. I have a field called "test5", and one of my script blocks looks like this: <SCRIPT>fld = new TextField(frm,"test5","test5","test");fld.cchMaxLength = "255";fld.cchDisplaySize = "";fld.IMEMode="";fld.BuildUI();</SCRIPT>. This script block is rendered by JavaScript on the client. Remember the OWS.JS file? One of its jobs is to render the properties in HTML. To go over the script block, there is a new field that is created, and the length and display size are set. IMEMode (Input Method Editor) enables users to enter and edit foreign character sets such as Chinese, French, or Korean characters. And finally, BuildUI puts it all together and adds it to a form object. The code in the OWS.JS is JavaScript and heavily uses the object-oriented features of the language. For the more adventurous, meandering through the OWS.JS file and understanding how it works would be time well spent. Keep in mind, though, that the content of the OWS.JS file should never be tampered with. The important thing to understand is that the fields are rendered on the client.

Code could be added to the bottom of the page to render the custom properties, and then the fields would be out of order. So code could be written that would render all the properties appropriately. But that's a lot of work, and it would have to be added to the EDITFORM.ASPX and UPLOAD.ASPX files, and editing properties in Word would still render the properties as single line of text properties.

Because we have the CustomJSUrl feature, we can add our own JavaScript file. Find the custom elements and replace them with our custom functionality. A very simple example like the one demonstrated here could put the select box right inline with the properties. Because custom fields are very complicated, we'll provide a button to open a modal window. Upon closing the window, we will set a display and internal value. The display value ensures the use of a proper selection and will not be stored. The internal value represents a key from a third-party system. In this case, it's the pubs database. In the real world, this information can be a key from any number of places such as PeopleSoft, Oracle Financials, or a homegrown application. We are shooting for a screen that looks like Figure 4.12.

Figure 4.12. Upload/Edit with custom field.


When the user clicks Select Author, a modal dialog window opens as in Figure 4.13.

Figure 4.13. Custom property data manipulation page.


Though it is fairly plain, the point still sings out. There is an element that will facilitate our custom property. This example has been of just one simple HTML element. This could be an entire worksheet or a way to search on several pieces of information to find a particular customer number. After the user selects an author and closes the window, the information is passed back to the parent window, and the fields are populated accordingly (see Figure 4.14). Notice there is no cancel button in the modal window (refer to Figure 4.13). In a real application, you should probably have one. To keep things simple, the cancel button has been left out.

Figure 4.14. Results populated for user convenience.


In Figure 4.14, just below the Select Author button is the ID for Abraham Bennet. User interfaces are nice when they all behave the same way. This technique will work the same for the web and for Word.

How is all this done? JavaScript. Make friends with JavaScript if you haven't already. Microsoft has leveraged as much object-oriented programming as JavaScript has to offer. There is a particular problem with the way JavaScript files are includedlet's see how to overcome this next peculiarity.

JavaScript Loader

Loading a custom JavaScript block is a nice feature. The file referenced in CustomJSUrl is loaded for any page that can benefit from custom JavaScript. Remember, OWS.JS is shipping code and cannot be modified. This custom JavaScript enables developers to add their own functionality. However, something terrible happens when the JavaScript file is includedcontext is lost for the included file. Earlier we added an attribute to the project node:

[View full width]

CustomJSUrl="/_layouts/[%=System.Threading.Thread.CurrentThread.CurrentUICulture.LCID%] /CPS/CustomJSLoader.aspx"


If we take out the preceding slash from the address, then the context is fine for the web and for Word because the JavaScript isn't included at all. Interestingly enough, JavaScript does have access to the correct context. The CUSTOMJSLOADER.ASPX file gets the context from JavaScript and passes on that information in the form of a parameter on a URL.

Listing 4.28. CustomJSLoader.aspx Snippet

[View full width]

 <%@ Page language="C#"     %> <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls"  Assembly="Microsoft.SharePoint, Version=11.0.0.0, Culture=neutral,  PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities"  Assembly="Microsoft.SharePoint, Version=11.0.0.0, Culture=neutral,  PublicKeyToken=71e9bce111e9429c" %> <%@ Import Namespace="Microsoft.SharePoint" %> <%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages"  Assembly="Microsoft.SharePoint, Version=11.0.0.0, Culture=neutral,  PublicKeyToken=71e9bce111e9429c" %> for (i=0;document.getElementsByTagName('script').length-1>=i;i++){  if (document.getElementsByTagName('SCRIPT')[i].src == '<%= Request .ServerVariables["PATH_INFO"]%>'){  document.getElementsByTagName('SCRIPT')[i].src = '/_layouts/<%=System.Threading.Thread .CurrentThread.CurrentUICulture.LCID%>/CPS/Custom_JS.aspx?context=' + escape(unescape (window.location));       } } 

PATH_INFO contains the path and page name of the page we are on. By looping through all the SCRIPT tags, we can find the SCRIPT block whose src is the same as PATH_INFO. This will yield the index of the script block that loaded CUSTOMJSLOADER.ASPX in the first place. After we find the index of the SCRIPT block, we will replace the source with our URL and parameter information that point to a JavaScript file that does the heavy lifting.

Unescaping and escaping can seem a little silly at first, but it is necessary. Parameter information in the URL is already escaped. If there wasn't an unescape first, then we would be escaping already escaped code, which turns out to be a mess.

Another point to note is that it seems like this code could be placed directly into the CustomJSUrl attribute. The folks at Microsoft realized that the CustomJSUrl attribute is meant for a URL, so they designed the system so that this attribute is URL-encoded when rendered. When the single quotes and double quotes become encoded, the JavaScript no longer functions properly.

The appendix includes a complete listing of the CUSTOM_JS.ASPX file that is used; for now, let's just jump right into the heart of it. The concept is to replace an existing HTML element through JavaScript. For this purpose, we will use the outerHTML property of the existing element.

Listing 4.29. Custom_JS.aspx Snippet

[View full width]

 for(var i=0; i<=CustomFieldList.length-1; i++){  if (CustomFieldList[i][1] == 'AuthorLookup'){   tmpElem = document.getElementById(this.frm.stFieldPrefix + CustomFieldList[i][0]);   if (tmpElem != null){    var idDisp = "disp" + this.frm.stFieldPrefix + CustomFieldList[i][0];    tmpElem.readOnly = true;    tmpElem.onfocus = "document.getElementById('btn" + tmpElem.name + "').focus();document .getElementById('btn" + tmpElem.name + "').select();";    strButton = "<INPUT tabindex= \"" + tmpElem.tabIndex + "\" id=\"btn" + tmpElem.name + " \" name=\"btn" + tmpElem.name + "\" TYPE=\"BUTTON\" VALUE=\"Select Author\" ONCLICK= \"showModal('" + tmpElem.name + "','" + CustomFieldList[i][0] + "');\">"; strDisplay =  "<textarea readonly class= \"" + tmpElem.className + "\"id=\"" + idDisp + "\" name=\"" +  idDisp + "\"></textarea>";    tmpElem.outerHTML = strButton + '<BR>' + tmpElem.outerHTML + '<BR>' + strDisplay;   }  } } 

A two-dimensional array, which we'll explore how to define in just a few pages, contains the internal name of the field and the field type. For each field type, there would be a separate if block. For the purposes of our example, we just have the one custom type, AuthorLookup.

Notice this.frm.stFieldPrefix. frm is a form object that we can use for our purposes and is actually defined in OWS.JS. stFieldPrefix is a prefix that is a constant and is prepended to each HTML element. tmpElem then becomes the HTML element in question. Whatever you can do in HTML, you can accomplish here. For example, the user should be able to get focus on the field. This might be a desired effect or not. If the business requires it, the interface could be changed quite radically. We went for a generic solution to solve most business problems, and the button and modal window is a fairly versatile technique. The point is that a fairly creative solution can be created. Finally, we set the outerHTML of the tmpElem with new and old HTML. This is important because the form validation will use this HTML later on, which brings up another reason why it's better to use this technique rather than a new web part. By leveraging all the existing functionality of the page, we don't have to worry about double clicks, form validation, or anything that is acquired for free from the page. Now let's look at where the CustomFieldList variable comes from.

The CustomFieldList is actually created from the server side and is rendered as a JavaScript array. This is one of the reasons why the context is so important. Because of the way the SharePoint object model works, it's important to know the site, the web, and the list in which you are interested. Suffice it to say that the URL and list variables are created early on, and we won't end this discussion without covering them in detail. Just know that the URL and list variables contain the necessary information and enable the code to execute as designed.

Listing 4.30. Custom_JS.aspx Snippet

[View full width]

 function getCustomFieldList(){  <%  if (loadCPS){  try{   SharePointBook.SPCustomField spCustomField = new SharePointBook.SPCustomField();   int count = 0;   string strOutput = "";   SPSite siteCollection = new SPSite(url);   SPWeb spWeb = siteCollection.OpenWeb();   SPList spList = spWeb.Lists[list];   SPFieldCollection spFields = spList.Fields;    foreach (SPField f in spFields) {     if (f.Type.ToString() == "Text"){      if(spCustomField.isCustomField(f)){       strOutput += "CustomFields [" + count + "] = new Array(2);";       strOutput += "CustomFields [" + count + "][0] = '" + f.InternalName + "';";       strOutput += "CustomFields [" + count + "][1] = '" + spCustomField.GetAttributeValue (f,"Format") + "';";       count++;      }     }    }    if (strOutput !=""){     strOutput = "CustomFields = new Array(" + count + ");" + strOutput;     strOutput += "return CustomFields;";    }    else{     strOutput = "return null;";    }    Response.Write (strOutput);   }   catch (Exception e){    Response.Write ("alert('!"+e.Message+"');");   }  }  %> return null; } 

One of the first things we do is to get the siteCollection, open the web and the list, and fetch the list of fields from the fields collection. We iterate through the list of fields, constructing a string that will be rendered to the client. There is some additional logic shown that will properly render the JavaScript code, but the important thing to realize is that the iteration through the fields collection ultimately creates a JavaScript array. The next thing to examine is the url and list variable initialization.

If the correct context were available, we wouldn't need a loader facility. But because it's not, it creates an interesting problem to solve. The first thing we need to do is break out all the parameters that were once available in the query string.

Listing 4.31. Custom_JS.aspx Snippet

 string myContext = Request.QueryString["Context"]; int idxQuery = myContext.IndexOf('?'); Hashtable hash = new Hashtable(); if (idxQuery>0) {  for (int i=0;i<param.Length;i++){   if (param[i].IndexOf('=')>0) {    nameValue = param[i].Split('=');    hash.Add(nameValue[0], nameValue[1]);   }  } } 

This code is a simple way to create a hashtable of name-value pairs so that we can simply use the parameters from the previous url.

It's pretty obvious from the web what the url is, but from Microsoft Word, nothing can be seen. Going to the IIS logs can help you solve a problem like this. There you can see the parameter information, the path info, and a myriad of other information. From there it is pretty easy to determine what Word was doing through the web. When the user attempts to save a file from Word to a url, Word will display a list of files at that url. After the user has chosen to save the file in that location, another modal dialog window appears with the list of properties. Both modal dialogs load CustomJSUrl. Looking at the information in IIS logs, it is easy to determine that Word passes the parameters location and dialogview. The dialogview that we are most interested in is "SaveForm". "SaveForm" causes the list of properties to show.

Listing 4.32. Custom_JS.aspx Snippet

[View full width]

 dialogview = hash.ContainsKey("dialogview") ? hash["dialogview"].ToString() : ""; location = hash.ContainsKey("location") ? hash["location"].ToString() : "" ; if (dialogview == "SaveForm"){  location = location.Remove(location.ToLower().IndexOf("/"), location.Length -location .ToLower().IndexOf("/"));  url = myContext.Remove(myContext.ToLower().LastIndexOf("/_vti_bin/owssvr.dll"), myContext .Length - myContext.ToLower().LastIndexOf("/_vti_bin/owssvr.dll")) + "/" + location; list = location;  loadCPS = true; } if (myContext.ToLower().IndexOf("/forms/editform.aspx") > 0){  url = myContext.Remove(myContext.ToLower().LastIndexOf("/forms/editform.aspx"), myContext .Length - myContext.ToLower().LastIndexOf("/forms/editform.aspx") );  list = url.Substring(url.LastIndexOf('/') + 1).ToString();  loadCPS = true; } if (myContext.ToLower().IndexOf("/forms/upload.aspx") > 0){  url = myContext.Remove(myContext.ToLower().LastIndexOf("/forms/upload.aspx"), myContext .Length - myContext.ToLower().LastIndexOf("/forms/upload.aspx"));  list = url.Substring(url.LastIndexOf('/') + 1).ToString();  loadCPS = true; } 

All three if blocks are dedicated to setting the value for url, list, and loadCPS. As we alluded to before, url and list are variables that help reconstruct the context. The loadCPS variable is a Boolean that will be used to allow or disallow code from running. Because the script block is loaded on just about every page, it's important to run code only when it is necessary. The first if block is dedicated to Word activity. The last two are used in the web application. To ensure uniqueness, the Forms folder and the filename were included. Although this code could be consolidated, we've repeated it here for clarity.

The display value is not stored, so SharePoint doesn't have any way to give us that data. If it's needed, then it must be fetched from the client side. Remember, that's where all this rendering magic takes placeputting a JavaScript web service to good use by fetching the data as needed. Much documentation is available on the web about the subject; we'll only be covering what it takes to make this example clear.

Author Lookup Web Service Calls

There are several pieces to the puzzle when it comes to JavaScript web services. First, a div tag must be rendered with a behavior pointing to webservice.htc. This file can be downloaded from Microsoft.com and is advertised as unsupported. The webservice.htc is a substantial file and takes some time to load. When loaded, an event is fired and a handler called. That handler can begin to load the Web Service Definition Language (WSDL). When the WSDL is done loading, yet another handler is fired. At this point, everything is finally ready for web service calls. In this last handler, which we'll call serviceAvailable(), the web service requests that fetch the author's name can be fired. A final event handler to handle the result, handleWebServiceResult(), will populate the values. The following is a look at the code out of context. The entire CUSTOM_JS.ASPX file is in the appendix.

Listing 4.33. Custom_JS.aspx Snippet

[View full width]

 var newText = document.createElement("<div onserviceavailable=\"serviceAvailable();\"  onreadystatechange=\"doneloading();\" id=\"divWebServiceCaller\" style=\"behavior:url (< %=url%>/_layouts/1033/CPS/webservice.htc)\"></div>"); document.body.appendChild(newText); function doneloading(){  if (document.getElementById("divWebServiceCaller").readyState=="complete"){   loadWebServices();  } } function loadWebServices() {  document.getElementById("divWebServiceCaller").useService("http://<%=host%>/_vti_bin /getAuthor.asmx?wsdl","GetAuthor"); } function serviceAvailable() {  var CustomFieldList = getCustomFieldList();  if (CustomFieldList != null){   for(var i=0; i<=CustomFieldList.length-1; i++){    tmpElem = document.getElementById(this.frm.stFieldPrefix + CustomFieldList[i][0]);    if (tmpElem != null){     if (CustomFieldList[i][1] == 'AuthorLookup'){      var idDisp = "disp" + this.frm.stFieldPrefix + CustomFieldList[i][0];      var idCall = document.getElementById("divWebServiceCaller").GetAuthor.callService (handleWebServiceResult, "ById", tmpElem.value);      hash.add (idCall, document.getElementById(idDisp));     }    }   }  } } function handleWebServiceResult(res) {  if (!res.error) {   var tmpElem = hash.get(res.id)   tmpElem.value = res.value;  }  else {   alert("Unsuccessful call. Error is " + res.errorDetail.string);  } } 

When serviceAvailable() is finally called, the array of custom fields is iterated through, just as before. For each iteration, the display field object and idCall are added to a hash table. The idCall is a return value from the web service call. This way, when a call back is made and handleWebServiceResult() is called, there is an easy way to figure out which call back is being answered. res.id is the same as the callID that we received earlier. Simply by looking up the callID in the hash table, we can set the value of the tmpElem, which is the textarea tag that displays the display value. What about that hash table? It's not very monumental.

Listing 4.34. Custom_JS.aspx Snippet

 function hashtable() {  this.add = mAdd;  this.get = mGet; } function mAdd(name, value) {  this[name] = value; } function mGet(strKeyName) {  return(this[strKeyName]); } var hash = new hashtable(); 

That's basically it. Because JavaScript is not strongly typed, anything can be stored as the value, including the HTML element object. The "Caller ID" is used as the key. The Caller ID is simply a consecutive auto number.

The rest is housekeeping, which is fundamentally everything. The code is listed in the appendix in its entirety. We've seen how to call a web service, but we haven't seen the web service itself.

GETAUTHOR.ASMX

GETAUTHOR.ASMX is the web service that is called from JavaScript to get the author's name. GETAUTHOR.ASMX is a gross representation of what that web service would look like. A call is made to our data services to get the name based on the ID passed in, and it returns the author's name.

Listing 4.35. GetAuthor.asmx

 using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Web; using System.Web.Services; using SharePointBook; namespace select {  public class getAuthor : System.Web.Services.WebService {   public getAuthor() {    InitializeComponent();   }   #region Component Designer generated code   //Required by the Web Services Designer   private IContainer components = null;   private void InitializeComponent(){}   protected override void Dispose( bool disposing ){    if(disposing && components != null){    components.Dispose();    }    base.Dispose(disposing);   }   #endregion   [WebMethod]   public string ById(string AuthorId){    Authors myExample = new Authors();    return myExample.GetAuthorByID(AuthorId);   }  } } 

It's easy to imagine all the different calls that might be made. But remember there's a performance hit for each web service call, even if it's only a burden for the client. On a properties page, it's not likely that you will have hundreds or even tens of custom properties that require a lookup that will use a web service; in all actuality, there will only be a few, so this burden is considered acceptable.

You might assume that a web service, which is a page with an .ASMX extension, would live in the Layouts folder. Not true. Web services are meant to be run from the ISAPI folder. Some changes can be made to the web.config file in the Layouts folder, but web services are meant to be run from the ISAPI folder, C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\ISAPI, in SharePoint. You must make a special preparation before you can run the web service.

Running a Web Service in SharePoint

Typically, a web service will return the WSDL by simply adding ?WSDL to the end of the url. Extra steps are needed to run a web service because of the enhanced security model used by Windows SharePoint Service. The first thing you should do is to create static .WSDL and .DISCO files. Using the command prompt provided by Visual Studio, navigate the file system to where the web service is. In the command prompt, enter

Disco http://server_name:Port/path/GetAuthor.asmx


Two files will be createdGetAuthor.disco and GetAuthor.wsdland they both need editing. First, they both need to be converted to ASP.NET pages. Open GetAuthor.disco and replace

<?xml version="1.0" encoding="utf-8"?>


with

[View full width]

<%@ Page Language="C#" Inherits="System.Web.UI.Page"%> <%@ Assembly Name="Microsoft .SharePoint, Microsoft.SharePoint, Version=11.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Import Namespace="Microsoft.SharePoint.Utilities" %> <%@ Import Namespace="Microsoft.SharePoint" %> <% Response.ContentType = "text/xml"; %>


Replace this line

[View full width]

<contractRef ref="http://server_name:Port/Path/GetAuthor.asmx?wsdl" docRef="http:/ /server_name:New_Port/Project_Name/Service1.asmx" xmlns="http://schemas.xmlsoap.org/disco /scl/" />


with

<contractRef ref=<% SPEncode.WriteHtmlEncodeWithQuote(Response, SPWeb.OriginalBaseUrl(Request)  + "?wsdl", '"'); %> docRef=<% SPEncode.WriteHtmlEncodeWithQuote(Response,  SPWeb.OriginalBaseUrl(Request), '"'); %> xmlns="http://schemas.xmlsoap.org/disco/scl/" /> 


Finally, replace

[View full width]

<soap address="http://server_name: Port/Path/GetAuthor.asmx" xmlns:q1="http://tempuri.org /" binding="q1:Service1Soap" xmlns="http://schemas.xmlsoap.org/disco/soap/" />


with

[View full width]

<soap address=<% SPEncode.WriteHtmlEncodeWithQuote(Response, SPWeb.OriginalBaseUrl (Request), '"'); %> xmlns:q1="http://tempuri.org/" binding="q1:Service1Soap" xmlns="http:/ /schemas.xmlsoap.org/disco/soap/" />


And save the file as GetAuthor Disco.aspx, where GetAuthor is the name of your web service. Next, open GetAuthor.wsdl and replace

<?xml version="1.0" encoding="utf-8"?>


with

[View full width]

<%@ Page Language="C#" Inherits="System.Web.UI.Page"%> <%@ Assembly Name="Microsoft .SharePoint, Microsoft.SharePoint, Version=11.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Import Namespace="Microsoft.SharePoint.Utilities" %> <%@ Import Namespace="Microsoft.SharePoint" %> <% Response.ContentType = "text/xml"; %>


Next, find the soap tag

 <soap:address location="http://server_name:Port/Path/GetAuthor.asmx" /> 


and replace it with

[View full width]

<soap:address location=<% SPEncode.WriteHtmlEncodeWithQuote(Response, SPWeb .OriginalBaseUrl(Request), '"'); %> />


And save the file as GetAuthor Wsdl.aspx. Ok, that was kind of boring, but after these two files are in place, you don't have to modify them much. The .WSDL file needs more love than the .DISCO file. The .DISCO file won't change often, but the .WSDL file will change every time you change a method signature or add a method. Other than that, it's pretty static. Now it's just a matter of putting them where they go. Copy the two .ASPX pages you just made, GETAUTHORWSDL.ASPX and GETAUTHORWSDL.ASPX, and the GETAUTHOR.ASMX file to C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\ISAPI. Copy the .DLL to the bin folder of C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\ISAPI as well.

The data access class needs to be added to the bin folder as well. This class could be added to the global assembly cache and called from there, but for simplicity in this example we just put it in the bin folder as well.

CUSTOMPROPERTYDATAMANIPULATION.ASPX

For each type of custom property, there should be an .ASPX page that will be opened in the modal window. CUSTOMPROPERTYDATA MANIPULATION.ASPX represents any page that would be used to present the custom property types to the user. A better name would indicate the property that is being used.

When the user clicks the Author Lookup button, a modal dialog is opened, and CUSTOMPROPERTYDATAMANIPULATION.ASPX is rendered. This page presents our user control to the user along with a button to close the window. In a real-world situation, you would probably have a cancel button, and your custom property type would be a little more complicated.

A quick note about Word and its modal windows is that Word caches the pages. In development, it can be quite frustrating to delete the cache from the browser every time you make a change. A common cure for caching involves the following lines of code:

Listing 4.36. Snippet to Prevent Caching

 Response.Cache.SetNoServerCaching(); Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache); Response.Cache.SetNoStore(); Response.Cache.SetExpires(new DateTime(1900, 1, 1, 0, 0, 0, 0)); 

This code will keep the page from caching in most cases except for when you get a syntax error. In that case, you need to delete the cache from Internet Explorer. The heart of this technique is retrieving the custom field schema from SharePoint and passing information to the user control. Using the same techniques as before, we will pass the context information and field of interest in the query string when the user clicks the Select Author button.

Listing 4.37. CustomPropertyDataManipulation.aspx

[View full width]

 <% string url = Request.QueryString["URL"]; string list = url.Substring(url.LastIndexOf('/') + 1).ToString(); string strField = Request.QueryString["InternalFieldName"]; SPSite siteCollection = new SPSite(url); SPWeb spWeb = siteCollection.OpenWeb(); SPList spList = spWeb.Lists[list]; SPFieldCollection spFields = spList.Fields; SPField spField = spFields.GetFieldByInternalName(strField); SharePointBook.SPCustomField spCustomField = new SharePointBook.SPCustomField(); AuthorLookup1.Style = spCustomField.GetCustomSetting(spField, "Style").ToString(); AuthorLookup1.DefaultValue = Request.QueryString["defaultValue"]; %> <TD><uc1:AuthorLookup  ElemToSet="myOutput" WhatToReturn="both"  runat="server"></uc1:AuthorLookup></TD> 

The url is parsed to retrieve the siteCollection and the library. The strField is populated from the query string as well. With that information, we can look up the custom properties of the field and pass the information to the user control.

We've used setting the text to red as an example of how to pass specific information to the user control. Anything could be passed back to the user control., though, not just style settings. A key could be passed to indicate what kind of data to return and from where. Perhaps there are several lookups that would be valuable, such as a query on publishers or books, or a query from some other database all together.




SharePoint 2003 Advanced Concepts. Site Definitions, Custom Templates, and Global Customizations
SharePoint 2003 Advanced Concepts: Site Definitions, Custom Templates, and Global Customizations
ISBN: 0321336615
EAN: 2147483647
Year: 2006
Pages: 64

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net