For my current assignment (a MOSS2007 project) I needed to customize the NewLink page and use a different control for the url field. I can imagine that typing in the url is no great user experience.
From:
To:
Especially, when the item is within the current Site Collection. The Asset Picker came into play for my solution…
Custom Field Type
I started to create a custom field type, named UrlBrowseField. This field inherits from SPFieldUrl. When you use SPFieldUrl instead of SPFieldText, you can automatically take advantage of the BackwardLinks and ForwardLinks functionality. And, when the document name is changed, all links to this document will be updated automatically as well. How great is that? So, here is my UrlBrowseField.cs class:
public UrlBrowseField(SPFieldCollection fields, string fieldName)
: base(fields, fieldName)
{ }
public UrlBrowseField(SPFieldCollection fields, string typeName, string displayName)
: base(fields, typeName, displayName)
{ }
public override BaseFieldControl FieldRenderingControl
{
get
{
// Create a new UrlBrowse field control
BaseFieldControl fieldControl = new UrlBrowseFieldTypeControl();
fieldControl.FieldName = this.InternalName;
return fieldControl;
}
}
public override object GetFieldValue(string value)
{
string url = value;
if (!String.IsNullOrEmpty(value))
{
// What’s the stored url?
if (value.IndexOf(",") > 0)
{
url = value.Substring(0, value.IndexOf(","));
}
}
SPFieldUrlValue fieldUrlValue = new SPFieldUrlValue();
fieldUrlValue.Url = url;
fieldUrlValue.Description = url;
return fieldUrlValue;
}
}
We also need a XML file defining our field type. This fldtype_UrlBrowseField.xml is in the XML folder of the 12 HiveTemplate folder. The content of the XML file is as follows:
<FieldType>
<Field Name="Filterable">TRUE</Field>
</FieldTypes>
For more information about creating custom field types, read it here on MSDN.
Next step is to create a custom control for the custom field type.
Custom Control
You might have noticed the override FieldRenderingControl method. You will need this when you want to implement your own UI for your custom field. I wanted to use the Asset Picker. It’s the UI window when you’re browsing for images within the Site Collection:
Back to the code. In the FieldRenderingControl method I create a new instance of UrlBrowseFieldTypeControl. Here’s how my UrlBrowseFieldTypeControl class looks like:
public class UrlBrowseFieldTypeControl : BaseFieldControl
{
public UrlBrowseFieldTypeControl()
{ }
protected AssetUrlSelector urlSelector;
protected HyperLink urlDisplay;
public override object Value
{
get
{
SPFieldUrlValue fieldUrlValue = new SPFieldUrlValue();
fieldUrlValue.Description = this.urlSelector.AssetUrl;
fieldUrlValue.Url = this.urlSelector.AssetUrl;
return fieldUrlValue;
}
set
{
SPFieldUrlValue fieldUrlValue = (SPFieldUrlValue)value;
this.urlSelector.AssetUrl = fieldUrlValue.Url;
}
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// Set the value if this is a postback.
if ((this.Page.IsPostBack) && (base.ControlMode == SPControlMode.Edit || base.ControlMode == SPControlMode.New))
{
SPFieldUrlValue fieldUrlValue = new SPFieldUrlValue();
fieldUrlValue.Url = this.urlSelector.AssetUrl;
fieldUrlValue.Description = this.urlSelector.AssetUrl;
this.ListItemFieldValue = fieldUrlValue;
}
}
protected override void CreateChildControls()
{
base.CreateChildControls();
// Add the asset picker when in edit or new mode.
if (base.ControlMode == SPControlMode.Edit || base.ControlMode == SPControlMode.New)
{
this.urlSelector = new AssetUrlSelector();
this.urlSelector.AllowExternalUrls = true;
this.urlSelector.UseImageAssetPicker = false;
this.Controls.Add(this.urlSelector);
}
if (base.ControlMode == SPControlMode.Display)
{
this.urlDisplay = new HyperLink();
this.Controls.Add(this.urlDisplay);
}
}
protected override void Render(HtmlTextWriter output)
{
if ((base.ControlMode == SPControlMode.Edit || base.ControlMode == SPControlMode.New) && this.ListItemFieldValue != null)
{
SPFieldUrlValue fieldUrlValue = (SPFieldUrlValue)this.ListItemFieldValue;
this.urlSelector.AssetUrl = fieldUrlValue.Url;
}
if (base.ControlMode == SPControlMode.Display && this.ListItemFieldValue != null)
{
SPFieldUrlValue fieldUrlValue = (SPFieldUrlValue)this.ListItemFieldValue;
this.urlDisplay.NavigateUrl = fieldUrlValue.Url;
this.urlDisplay.Text = Properties.Resources.LinkDisplayDescription;
}
base.Render(output);
}
}
Okay, now we have a fully working custom field type and control. You can use it for your metadata columns. But wait! There’s more…
Custom Content Type derived from Link to a Document
It would be nice to have the same functionality when you use the Link To A Document content type. When adding a new item of that content type SharePoint opens the page NewLink.aspx for you. You can enter a name for the link and you have to type in the url.
Let’s replace this with our own NewLink page using our custom UrlBrowseField.
Before doing so, I created a new content type that inherits from Link To A Document. With this new content type I also defined a document template: the new and custom NewLink page. My custom content type is defined like this:
<ContentType ID="0x01010A00DA297AFEACC0F346BD6FC0AF7EC86A2E" Name="My Link To A Document" Group="Demo" Description="Custom link to a document">
<FieldRefs>
<RemoveFieldRef ID="{c29e077d-f466-4d8e-8bbe-72b66c5f205c}" Name="Url"/>
<FieldRef ID="{9a4a0096-ff20-4d06-9037-5496606097e6}" Name="AdditionalComments" />
<FieldRef ID="{726CE043-B9FA-42D8-9DC1-5BFDC6AE0E66}" Name="UrlBrowse" Required="TRUE"/>
</FieldRefs>
<DocumentTemplate TargetName="/_layouts/Demo/doctemplates/NewLink.aspx" />
</ContentType>
Notice the RemoveFieldRef: I do not want to use the original url field in the Link To A Document content type. The site column UrlBrowse is defined like this:
<Field Type="UrlBrowseField" DisplayName="Url Browse" Required="TRUE" Group="Demo" ID="{726CE043-B9FA-42D8-9DC1-5BFDC6AE0E66}" StaticName="UrlBrowse" Name="UrlBrowse"></Field>
Now it’s time to create our own custom NewLink.aspx page.
Custom NewLink page
Copy the original NewLink.aspx page from 12HiveTemplatesLayouts to your folder _layouts/Demo/doctemplates.
Now, when you open your version of NewLink.aspx you will see several Register directives, some Javascript and ASP.NET controls. Nothing to worry about. Let’s find the control for the url field:
<wssawc:InputFormTextBoxTitle="<%$Resources:wss,newlink_url%>"class="ms-input"ID="UrlInput"Columns="35"Runat="server"maxlength=255width=300 />
I replaced it with:
<demo:UrlBrowseFieldTypeControl ID="UrlBrowseInput" runat="server" ControlMode="New" FieldName="UrlBrowse" />
and I registered the assembly as well for the demo prefix:
<%@ Register Tagprefix="demo" Namespace="Demo.FieldTypes" Assembly="Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1f06917529e07c91" %>
Are we there yet? No, because there are some references to the original UrlInput field ID in the page. You have to replace them as well with your field ID. In our case UrlBrowseInput. Because our control is “complex” we also need to modify the JavaScript code. The original code says:
var folderUrl = form.<%SPHttpUtility.NoEncode(UrlBrowseInput.ClientID,Response.Output);%>.value;
When running the JavaScript (by clicking the link Click here to test) you will get an error. If you analyse the rendered HTML code you will see this:
var folderUrl = form.ctl00_PlaceHolderMain_ctl01_ctl01_UrlBrowseInput.value;
:
As you can see the JavaScript references the wrong element. You need to reference the input-element with ID ctl00_PlaceHolderMain_ctl01_ctl01_UrlBrowseInput_ctl00_AssetUrlInput.
Now, however, if you package and deploy your solution and test it all, you will still get some errors. Let me explain what goes on behind the curtains. First of all, the original UrlInput field was based on TextBox and ours is UrlBrowseField. We need to change the Javascript. Second, the NewLink page has code behind. When using .NET Reflector and open the Microsoft.SharePoint.ApplicationPages assembly and go to NewLinkPage, you can see what’s going on in the onOK event. And yes, it’s referencing the field UrlInput of type TextBox.
Not going to work. We need to write our own applicationpage for our NewLink.aspx.
Custom ApplicationPage for the NewLink page
In our NewLink.aspx page I changed the <Page> directive:
<%@ Page Language="C#" Inherits="Demo.ApplicationPages.NewLinkPage" MasterPageFile="~/_layouts/application.master" %>
Then I created a new applicationpage NewLinkPage in the solution and copied all existing code from the .NET Reflector’s NewLinkPage and modified it to my needs. Be aware the .NET Reflector can produce lines of code with lot’s of GoTo statements. Of course, you want to refactor that.
public class NewLinkPage : LayoutsPageBase
{
public SPDocumentLibrary doclib;
protected TextBox NameInput;
protected HyperLink TitleLabel;
protected UrlBrowseFieldTypeControl UrlBrowseInput;
private SPFolder m_folder;
private string m_folderUrl;
private SPList m_list;
private SPFolder CurrentFolder
{
get
{
if (this.m_folder == null)
{
this.m_folder = base.Web.GetFolder(this.CurrentFolderServerRelativeUrl);
}
return this.m_folder;
}
}
private string CurrentFolderServerRelativeUrl
{
get
{
if (this.m_folderUrl == null)
{
string str = base.Request.QueryString["RootFolder"];
if (string.IsNullOrEmpty(str))
{
this.m_folderUrl = this.CurrentList.RootFolder.Url; }
else
{
this.m_folderUrl = str;
}
}
return this.m_folderUrl;
}
}
private SPList CurrentList
{
get
{
if (this.m_list == null)
{
SPWeb contextWeb = SPControl.GetContextWeb(this.Context);
Guid guid = new Guid(base.Request.QueryString["List"]);
this.m_list = contextWeb.Lists[guid];
}
return this.m_list;
}
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
SPWeb contextWeb = SPControl.GetContextWeb(HttpContext.Current);
ThrowIfNoListQueryString();
SPList list = contextWeb.Lists.GetList(new Guid(base.Request.QueryString.GetValues("List")[0]), true);
this.doclib = (SPDocumentLibrary)list;
this.TitleLabel.Text = SPHttpUtility.HtmlEncode(list.Title);
this.TitleLabel.NavigateUrl = SPHttpUtility.UrlPathEncode(list.DefaultViewUrl, true);
}
public void onOK(object sender, EventArgs e)
{
SPWeb rootWeb = SPControl.GetContextSite(HttpContext.Current).RootWeb;
SPContentTypeId id = new SPContentTypeId(base.Request.QueryString["ContentTypeID"]);
SPContentType type = this.CurrentList.ContentTypes[id];
SPFolder currentFolder = this.CurrentFolder;
SPFileCollection files = currentFolder.Files;
string urlOfFile = currentFolder.Url + "/" + this.NameInput.Text + ".aspx";
// Copied the original format string to a resource file
string format = Properties.Resources.LinkASPXContent;
StringBuilder builder = new StringBuilder(format.Length + 400);
builder.AppendFormat(format, typeof(SPDocumentLibrary).Assembly.FullName);
// Provide some metadata for the link item
Hashtable properties = new Hashtable();
properties.Add("ContentType", type.Name);
// Add the file with metadata to the library
SPFile file = files.Add(urlOfFile, new UTF8Encoding().GetBytes(builder.ToString()),properties, false);
SPListItem item = file.Item;
item["UrlBrowse"] = this.UrlBrowseInput.Value;
item.UpdateOverwriteVersion();
// Does the content type have more then 2 fields (Title and Content Type)? If yes then redirect to EditForm page, otherwise check in
if (this.numFields_NoTitle(type) <= 2)
{
if (file.CheckOutStatus == SPFile.SPCheckOutStatus.None)
{
SPUtility.Redirect(null, SPRedirectFlags.UseSource, this.Context);
}
}
else
{
SPUtility.Redirect(this.CurrentList.Forms[PAGETYPE.PAGE_EDITFORM].ServerRelativeUrl + string.Concat(new object[] { "?Mode=Upload&CheckInComment=&ID=", file.Item.ID, "&Source=", base.Request.QueryString["Source"], "&RootFolder=", currentFolder.ServerRelativeUrl }), SPRedirectFlags.Static, this.Context);
return;
}
file.CheckIn("");
SPUtility.Redirect(null, SPRedirectFlags.UseSource, this.Context);
}
/// <summary>
/// Counts the number of fields in the contenttype excluding the standard Title field
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private int numFields_NoTitle(SPContentType type)
{
int num = 0;
foreach (SPField current in type.Fields)
{
if (current.Hidden) continue;
if (!current.ReadOnlyField)
{
if (current.Title == "Title") continue;
if (current.Title == "Content Type") continue;
num++;
}
}
return num;
}
/// <summary>
/// Throws an exception when no list is defined in the query string.
/// </summary>
private void ThrowIfNoListQueryString()
{
string[] values = HttpContext.Current.Request.QueryString.GetValues("List");
if ((values == null) || (values.Length == 0))
{
throw new SPException(SPResource.GetString("InvalidQueryString", new object[] { "List" }));
}
}
}
We are almost there. You could test it all now, but when you click the link, nothing happens. Remember the format string in the onOK event method? It contains some HTML and ASP.NET code:
<%@ Assembly Name='{0}’ %>
<%@ Register TagPrefix=’Demo’ Namespace=’Demo.Controls’ Assembly=’Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1f06917529e07c91′ %>
<html>
<Head> <META Name=’progid’ Content=’SharePoint.Link’>
<body>
<Demo:UrlRedirector runat=’server’ Fieldname=’UrlBrowse’ />
</body>
</html>
The original code with the SharePoint:UrlDirector is based on the TextBox. Ours is not. So, we need to write our own UrlDirector control based on SPFieldUrl:
public class UrlRedirector: Control
{
public string Fieldname { get; set; }
protected override void OnInit(EventArgse)
{
base.OnInit(e);
if(!String.IsNullOrEmpty(Fieldname))
{
SPListItem item = SPContext.Current.ListItem;
if(item.Fields.ContainsField(Fieldname))
{
SPFieldUrlValue fieldUrlValue = item[Fieldname] as SPFieldUrlValue;
if( fieldUrlValue != null)
if(!String.IsNullOrEmpty(fieldUrlValue.Url))
{
Page.Response.Redirect(fieldUrlValue.Url );
}
}
}
}
}
That should do it. Now we have our customized NewLink page with a Url input field where you can browse to your document and have the url stored instead of typing it in.
Can this be accomplished without a farm solution on 2010? is it easier on 2013? Do you mind outlining the approach maybe as a blog post?
Bill, you cannot do this without a farm solution for both 2010 and 2013.