Notes for the Creating Highly Usable & Distributable Sitefinity Controls webinar:
Create a new Class Library Project
- Create new Class Library project in webinar folder.
- Remove Class1.cs
Creating a Basic HelloWorld CompositeControl
Create ~/HelloWorld.cs
using System.Web.UI;
namespace SitefinityWatch.Web.UI
{
public class HelloWorld : System.Web.UI.WebControls.CompositeControl
{
protected override void CreateChildControls()
{
this.Controls.Add(new LiteralControl("<strong>Hello World!</strong>"));
}
}
}
- Add a reference to System.Web
- Add this project to Sitefinity web site references
- Add a <toolboxControls> reference to ~/web.config:
<add name="Hello World" section="Sitefinity Watch" type="SitefinityWatch.Web.UI.HelloWorld, SitefinityWatch.Web.UI" description="Displays Hello world!" />
- Drag & drop this new control onto a Sitefinity page.
Creating a Basic SubLinks Control
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace SitefinityWatch.Web.UI
{
public class SubLinks : System.Web.UI.WebControls.CompositeControl
{
protected override void CreateChildControls()
{
if (SiteMap.CurrentNode.ChildNodes.Count > 0)
{
this.Controls.Add(new LiteralControl("<ul>"));
foreach (SiteMapNode node in SiteMap.CurrentNode.ChildNodes)
{
this.Controls.Add(new LiteralControl("<li>"));
var hyperLink = new HyperLink();
hyperLink.Text = node.Title;
hyperLink.NavigateUrl = node.Url;
this.Controls.Add(hyperLink);
this.Controls.Add(new LiteralControl("</li>"));
}
this.Controls.Add(new LiteralControl("</ul>"));
}
}
}
}
- Compile
- Add <toolboxControl> reference to ~/web.config
<add name="Sub-Links" section="Sitefinity Watch" type="SitefinityWatch.Web.UI.SubLinks, SitefinityWatch.Web.UI" description="Creates links to a page's children." />
Adding Control Attributes & Properties
- Add a Columns property
- Add a using System.ComponentModel;
- Add a Category attribute
- Add a DefaultProperty attribute
using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace SitefinityWatch.Web.UI
{
[DefaultProperty("Columns")]
public class SubLinks : System.Web.UI.WebControls.CompositeControl
{
protected override void CreateChildControls()
{
if (SiteMap.CurrentNode.ChildNodes.Count > 0)
{
Controls.Add(new LiteralControl("<ul>"));
foreach (SiteMapNode node in SiteMap.CurrentNode.ChildNodes)
{
Controls.Add(new LiteralControl("<li>"));
var link = new HyperLink();
link.NavigateUrl = node.Url;
link.Text = node.Title;
Controls.Add(link);
Controls.Add(new LiteralControl("</li>"));
}
Controls.Add(new LiteralControl("</ul>"));
}
}
[Category("Main")]
[Description("The number of columns to display sub-links into.")]
public int Columns
{
get
{
return _columns;
}
set
{
if (value <= 0)
_columns = 1;
else
_columns = value;
}
}
private int _columns = 1;
}
}
Make Columns Property Work
Evolve SubLinks contain the following code:
protected override void CreateChildControls()
{
SiteMapNode startNode = SiteMap.CurrentNode;
if (startNode.ChildNodes.Count > 0)
{
// Use the Columns property to determine the number of items per column.
decimal itemsInColumn = Math.Ceiling((decimal)startNode.ChildNodes.Count / (decimal)Columns);
int itemCount = 0;
this.Controls.Add(new LiteralControl("<div>"));
this.Controls.Add(new LiteralControl("<ul>"));
foreach (SiteMapNode node in startNode.ChildNodes)
{
itemCount++;
if (itemCount > itemsInColumn)
{
itemCount = 1;
itemsInColumn = Math.Round((decimal)startNode.ChildNodes.Count / (decimal)Columns);
this.Controls.Add(new LiteralControl("</ul>"));
this.Controls.Add(new LiteralControl("<ul>"));
}
this.Controls.Add(new LiteralControl("<li>"));
this.Controls.Add(CreateHyperlink(node));
this.Controls.Add(new LiteralControl("</li>"));
}
this.Controls.Add(new LiteralControl("</ul>"));
this.Controls.Add(new LiteralControl("</div>"));
}
}
/// <summary>
/// Creates a Hyperlink to a sub linked page.
/// </summary>
private HyperLink CreateHyperlink(SiteMapNode node)
{
var hyperLink = new HyperLink();
hyperLink.Text = node.Title;
hyperLink.NavigateUrl = node.Url;
return hyperLink;
}
- Add a using System.ComponentModel;
- Add a using System;
Embed Stylesheets
- Create ~/Resources/sitefinitywatch.css
- Set Build Action to Embedded Resource
- Add a resource mapping to ~/Properties/AssemblyInfo.cs:
[assembly: System.Web.UI.WebResource("SitefinityWatch.Web.UI.Resources.sitefinitywatch.css", "text/css")]
Add the following styles to ~/Resources/sitefinitywatch.css:
.sitefinitywatch_sublinks
{
padding: 15px 0 15px 0;
}
.sitefinitywatch_sublinks ul
{
padding-bottom: 0px;
line-height: 1.3;
list-style-type: none;
margin: 0px;
padding-left: 0px;
padding-right: 0px;
float: left;
padding-top: 0px;
}
.sitefinitywatch_sublinks li
{
list-style-type: none;
width: 130px;
display: block;
margin: 5px;
}
Add the following method to ~/SubLinks.cs:
/// <summary>
/// Handles adding a stylesheet reference to the page's <head> tag
/// </summary>
private void EmbedStyles()
{
// Getting my own reference to System.Web.UI.Page to ensure it exists.
// Using this.Page instead will result in a null object in the Sitefinity Page Editor.
var page = (System.Web.UI.Page)System.Web.HttpContext.Current.Handler;
HtmlLink link = new HtmlLink();
link.Href = page.ClientScript.GetWebResourceUrl(this.GetType(), "SitefinityWatch.Web.UI.Resources.sitefinitywatch.css");
link.Attributes.Add("type", "text/css");
link.Attributes.Add("rel", "stylesheet");
page.Header.Controls.Add(link);
}
Add a using System.Web.UI.HtmlControls;
Add a UseEmbeddedStyles and CssClass property:
[Category("Appearance")]
public bool UseEmbeddedStyles
{
get
{
return _useembeddedstyles;
}
set
{
_useembeddedstyles = value;
}
}
[Category("Appearance")]
public override string CssClass
{
get
{
return _cssClass;
}
set
{
_cssClass = value;
}
}
Add the following private values:
private string _cssClass = "sitefinitywatch_sublinks";
private bool _useembeddedstyles = true;
Add the following logic to the beginning of CreateChildControls()
// If using embedded stylesheets, then include embedded style in <head>
if (UseEmbeddedStyles)
EmbedStyles();
Change the <div> tag to reference the CssClass property:
this.Controls.Add(new LiteralControl("<div class=\"" + CssClass + "\">"));
Allow a Start Node Mode option to be Selected
Add a StartNodeMode property to SubLinks.cs:
[Category("Main")]
public StartNodeModes StartNodeMode
{
get
{
return _StartNodeMode;
}
set
{
_StartNodeMode = value;
}
}
Add an enumerator to SubLinks.cs:
public enum StartNodeModes { CurrentPage, SelectedPageId }
Add private _startnodemodes value:
private StartNodeModes _StartNodeMode;
Add the following PageId property to SubLinks.cs:
[Category("Main")]
public Guid PageId { get; set; }
Create a Custom WebEditor & Attach to PageId Property
Decorate the PageId property with the WebEditor attribute:
[Telerik.Cms.Web.UI.WebEditor("SitefinityWatch.Web.UI.Editors.PageIdSelector, SitefinityWatch.Web.UI")]
- Add an assembly reference to Telerik.Cms
- Add an assembly reference to Telerik.Cms.Web.UI
- Add an assembly reference to Telerik.Framework
- Add an assembly reference to Telerik.Security
- Add an assembly reference to Telerik.Web.UI
- Add an assembly reference to System.Configuration
- Add an assembly reference to System.Web.Extensions
- Change Copy Local property to False for all assembly references
Create ~/Editors/PageIdSelector.cs:
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using Telerik.Web.UI;
using Telerik.Cms;
using Telerik.Cms.Web;
namespace SitefinityWatch.Web.UI.Editors
{
public class PageIdSelector : Telerik.Cms.Web.UI.WebUITypeEditor<Guid>
{
protected override void CreateChildControls()
{
base.CreateChildControls();
}
public override Guid Value
{
get
{
return _value;
}
set
{
_value = value;
}
}
private Guid _value = Guid.Empty;
}
}
Add RadTree to CreateChildControls method:
this.Controls.Add(new LiteralControl("<div class=\"sitemapTree\">"));
RadTreeNode root = new RadTreeNode();
root.Text = "All Pages";
root.Value = Guid.Empty.ToString();
root.ExpandMode = TreeNodeExpandMode.ServerSideCallBack;
root.Expanded = true;
if (Value == Guid.Empty)
{
root.Selected = true;
}
tree = new RadTreeView();
tree.Nodes.Add(root);
this.Controls.Add(tree);
this.LoadNodes(root);
this.Controls.Add(tree);
this.Controls.Add(new LiteralControl("</div>"));
Add LoadNodes method:
private void LoadNodes(RadTreeNode root)
{
foreach (ICmsPage page in manager.GetPages(new Guid(root.Value)))
{
RadTreeNode node = new RadTreeNode();
node.Value = page.ID.ToString();
node.Text = page.MenuName;
node.ToolTip = page.MenuName;
node.Expanded = true;
root.Nodes.Add(node);
}
}
Add OnInit:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
this.manager = new CmsManager();
cmsSiteMap = (CmsSiteMapProvider)System.Web.SiteMap.Provider;
}
Add private variables:
private RadTreeView tree;
private CmsManager manager;
private CmsSiteMapProvider cmsSiteMap;
Compile & Run
Add the Node Expand Event Handler after new RadTreeView():
tree.NodeExpand += new RadTreeViewEventHandler(tree_NodeExpand);
Add Event Handler:
void tree_NodeExpand(object sender, RadTreeNodeEventArgs e)
{
this.LoadNodes(e.Node);
}
Add ExpandMode to LoadNodes method:
if (page.Pages.Count > 0)
{
node.ExpandMode = TreeNodeExpandMode.ServerSideCallBack;
}
Finish the WebEditor
Here is the completed ~/Editors/PageIdSelector.cs
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using Telerik.Web.UI;
using Telerik.Cms;
using Telerik.Cms.Web;
namespace SitefinityWatch.Web.UI.Editors
{
public class PageIdSelector : Telerik.Cms.Web.UI.WebUITypeEditor<Guid>
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
this.manager = new CmsManager();
cmsSiteMap = (CmsSiteMapProvider)System.Web.SiteMap.Provider;
}
protected override void CreateChildControls()
{
parentPages = new List<Guid>();
if (Value != Guid.Empty)
{
SiteMapNode selectedNode = cmsSiteMap.FindSiteMapNodeFromKey(Value.ToString());
parentPages = GetParents(selectedNode.ParentNode, parentPages);
parentPages.Reverse();
}
this.Controls.Add(new LiteralControl("<div class=\"sitemapTree\">"));
RadTreeNode root = new RadTreeNode();
root.Text = "All Pages";
root.Value = Guid.Empty.ToString();
root.ExpandMode = TreeNodeExpandMode.ServerSideCallBack;
root.Expanded = true;
if (Value == Guid.Empty)
{
root.Selected = true;
}
tree = new RadTreeView();
tree.NodeExpand += new RadTreeViewEventHandler(tree_NodeExpand);
tree.Nodes.Add(root);
this.Controls.Add(tree);
this.LoadNodes(root);
this.Controls.Add(tree);
this.Controls.Add(new LiteralControl("</div>"));
}
public void PreselectValue()
{
foreach (Guid pageID in parentPages)
{
RadTreeNode node = tree.FindNodeByValue(pageID.ToString());
node.Expanded = true;
if (node.Nodes.Count == 0)
{
LoadNodes(node);
}
}
}
private void LoadNodes(RadTreeNode root)
{
foreach (ICmsPage page in manager.GetPages(new Guid(root.Value)))
{
RadTreeNode node = new RadTreeNode();
node.Value = page.ID.ToString();
node.Text = page.MenuName;
node.ToolTip = page.MenuName;
if (page.Pages.Count > 0)
{
node.ExpandMode = TreeNodeExpandMode.ServerSideCallBack;
}
if (page.ID == Value)
{
node.Selected = true;
}
node.Expanded = true;
if (parentPages.Contains(page.ID))
{
LoadNodes(node);
}
root.Nodes.Add(node);
}
}
private List<Guid> GetParents(SiteMapNode node, List<Guid> Parents)
{
if (node != node.RootNode)
{
var cmsNode = (CmsSiteMapNode)node;
Parents.Add(cmsNode.PageID);
Parents = GetParents(node.ParentNode, Parents);
}
return Parents;
}
void tree_NodeExpand(object sender, RadTreeNodeEventArgs e)
{
this.LoadNodes(e.Node);
}
public override Guid Value
{
get
{
try
{
return new Guid(tree.SelectedValue);
}
catch
{
return _value;
}
}
set
{
_value = value;
}
}
private CmsManager manager;
private CmsSiteMapProvider cmsSiteMap;
private Guid _value = Guid.Empty;
private RadTreeView tree;
private List<Guid> parentPages;
}
}
Alter SubLinks.cs to use PageId and NodeModeSelector
Replace the following line in CreateChildControls:
SiteMapNode startNode = SiteMap.CurrentNode;
With this:
SiteMapNode startNode = GetStartingNode();
Create the following GetStartingNode() method:
/// <summary>
/// Finds the "starting" node that will be used to find child links.
/// </summary>
private SiteMapNode GetStartingNode()
{
if (StartNodeMode == StartNodeModes.SelectedPageId)
{
if (PageId == Guid.Empty)
{
return SiteMap.RootNode;
}
else
{
CmsSiteMapProvider cmsMap = (CmsSiteMapProvider)SiteMap.Provider;
return cmsMap.FindSiteMapNodeFromKey(PageId.ToString());
}
}
else
{
return SiteMap.CurrentNode;
}
}
Add a using Telerik.Cms.Web;
- Recompile & Run
- Select a page using the WebEditor
- Change SelectNodeMode to SelectedPage
Attach a Control Designer to the SubLinks Control
Decorate the SubLinks class with the following ControlDesigner attribute:
[ControlDesigner("SitefinityWatch.Web.UI.Design.SubLinksDesigner")]
Add the following using reference:
using Telerik.Framework.Web.Design;
Create ~/Design/SubLinksDesigner.cs:
using SitefinityWatch.Web.UI.Editors;
using System;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using Telerik.Framework.Web;
namespace SitefinityWatch.Web.UI.Design
{
public class SubLinksDesigner : Telerik.Framework.Web.Design.ControlDesigner
{
protected override void CreateChildControls()
{
textbox = new TextBox();
Controls.Add(textbox);
}
public override void OnSaving()
{
((SubLinks)DesignedControl).PageId = new Guid(textbox.Text);
}
private TextBox textbox;
}
}
Use a Template for Layout in SubLinks Control Designer
Create ~/Design/SublinksDesigner.ascx:
<%@ Register Namespace="Telerik.Web.UI" Assembly="Telerik.Web.UI" TagPrefix="telerik" %>
<%@ Register Namespace="SitefinityWatch.Web.UI.Editors" Assembly="SitefinityWatch.Web.UI" TagPrefix="sw" %>
<div class="ControlDesigner">
<h2>Create a list of dynamic sub-links:</h2>
<div class="fieldOption">
<asp:Label ID="Label1" EnableViewState="false" AssociatedControlID="StartNodeMode"
runat="server">Show sub-links for:</asp:Label>
<asp:DropDownList ID="StartNodeMode" runat="server">
<asp:ListItem Value="CurrentPage" Text="Current Page" />
<asp:ListItem Value="SelectedPageId" Text="Selected Page" />
</asp:DropDownList>
</div>
<div class="fieldOption">
<asp:Label ID="Label2" EnableViewState="false" AssociatedControlID="Columns" runat="server">Number of columns:</asp:Label>
<asp:DropDownList ID="Columns" runat="server">
<asp:ListItem Value="1" />
<asp:ListItem Value="2" />
<asp:ListItem Value="3" />
<asp:ListItem Value="4" />
<asp:ListItem Value="5" />
</asp:DropDownList>
</div>
<asp:Panel ID="PageIdSelectorPanel" Visible="false" runat="server">
<h3>Select the page that is a parent to the list of links:</h3>
<div class="treeOption"><sw:PageIdSelector ID="SiteMapTree" runat="server" /></div>
</asp:Panel>
</div>
Change Build Action to Embedded Resource
Add ~/Resources/designer.css:
.ControlDesigner
{
border-bottom: #e2e8e9 10px solid;
position: absolute;
border-left: #e2e8e9 10px solid;
overflow-x: hidden;
overflow-y: hidden;
margin: 0px;
bottom: 40px;
background: #e8edee;
overflow: hidden;
border-top: #e2e8e9 10px solid;
top: 38px;
right: 5px;
border-right: #e2e8e9 10px solid;
left: 5px
}
.ControlDesigner h2
{
padding-bottom: 10px;
}
.ControlDesigner h3
{
font-weight: bold;
padding-top: 8px;
padding-bottom: 5px;
}
.ControlDesigner .fieldOption
{
clear: both;
padding: 5px;
}
.ControlDesigner .fieldOption label
{
text-align: right;
float: left;
width: 150px;
font-size: 13px;
padding-right: 10px;
}
.ControlDesigner .fieldOption input
{
font-size: 13px;
}
.ControlDesigner .treeOption
{
border: solid 1px #5B777D;
height: 300px;
background-color: White;
overflow-y: auto;
overflow: auto;
}
Change Build Action to Embedded Resource
Add [assembly] to ~/Properties/AssemblyInfo.cs:
[assembly: System.Web.UI.WebResource("SitefinityWatch.Web.UI.Resources.designer.css", "text/css")]
Add the following to the CreateChildControls() method in ~/Designer/SubLinksDesigner.cs:
HtmlLink link = new HtmlLink();
link.Href = this.Page.ClientScript.GetWebResourceUrl(this.GetType(), "SitefinityWatch.Web.UI.Resources.designer.css");
link.Attributes.Add("type", "text/css");
link.Attributes.Add("rel", "stylesheet");
this.Page.Header.Controls.Add(link);
Control control = ControlUtils.GetTemplateFromResource("SitefinityWatch.Web.UI.Design.SubLinksDesigner.ascx", this.GetType());
Controls.Add(control);
Add using System.Web.UI;
Compile & Run
Add Actions for Templated Controls
Add the following class to ~/Designer/SubLinksDesigner.cs:
public class SubLinkDesignerContainer : Telerik.Cms.Web.UI.GenericContainer
{
}
Add the following properties/methods to SubLinkDesignerContainer:
public class SubLinkDesignerContainer : Telerik.Cms.Web.UI.GenericContainer
{
public SubLinkDesignerContainer()
{
Controls.Add(ControlUtils.GetTemplateFromResource("SitefinityWatch.Web.UI.Design.SubLinksDesigner.ascx", this.GetType()));
}
public Panel PageIdSelectorPanel
{
get
{
return base.GetControl<Panel>("PageIdSelectorPanel", true);
}
}
public PageIdSelector SiteMapTree
{
get
{
return base.GetControl<PageIdSelector>("SiteMapTree", true);
}
}
public DropDownList Columns
{
get
{
return base.GetControl<DropDownList>("Columns", true);
}
}
public DropDownList StartNodeMode
{
get
{
return base.GetControl<DropDownList>("StartNodeMode", true);
}
}
}
Replace the following methods/properties in SubLinksControlDesigner.cs:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
sublinks = (SubLinks)DesignedControl;
}
protected override void CreateChildControls()
{
HtmlLink link = new HtmlLink();
link.Href = this.Page.ClientScript.GetWebResourceUrl(this.GetType(), "SitefinityWatch.Web.UI.Resources.designer.css");
link.Attributes.Add("type", "text/css");
link.Attributes.Add("rel", "stylesheet");
this.Page.Header.Controls.Add(link);
container = new SubLinkDesignerContainer();
container.StartNodeMode.Items.FindByValue(sublinks.StartNodeMode.ToString().Trim()).Selected = true;
container.Columns.Items.FindByValue(sublinks.Columns.ToString().Trim()).Selected = true;
container.StartNodeMode.SelectedIndexChanged += new EventHandler(StartNodeMode_SelectedIndexChanged);
container.StartNodeMode.AutoPostBack = true;
container.SiteMapTree.Value = sublinks.PageId;
SetSiteMapTreeVisibility();
this.Controls.Add(container);
}
public override void OnSaving()
{
sublinks.Columns = Convert.ToInt32(container.Columns.SelectedValue);
if (container.StartNodeMode.SelectedValue == SubLinks.StartNodeModes.CurrentPage.ToString())
{
sublinks.StartNodeMode = SubLinks.StartNodeModes.CurrentPage;
}
else
{
sublinks.StartNodeMode = SubLinks.StartNodeModes.SelectedPageId;
sublinks.PageId = container.SiteMapTree.Value;
}
}
void StartNodeMode_SelectedIndexChanged(object sender, EventArgs e)
{
SetSiteMapTreeVisibility();
}
void SetSiteMapTreeVisibility()
{
if (container.StartNodeMode.SelectedValue == SubLinks.StartNodeModes.CurrentPage.ToString())
{
container.PageIdSelectorPanel.Visible = false;
}
else
{
container.PageIdSelectorPanel.Visible = true;
}
}
private SubLinkDesignerContainer container;
private SubLinks sublinks;
Resolving Assembly Version Dependencies
When you reference strong-named assembly, by default Visual Studio adds full reference to the referenced assembly. That means it includes the name of the assembly, the exact version, the culture and the public key token. If any of this information don't match the described exception is thrown.
Removing the strong-names of our assemblies is simply not an option so, you have two options to workaround building against every version of the assemblies you are referencing.
1. You could do partial referencing. See this article: http://msdn.microsoft.com/en-us/library/0a7zy9z5(VS.71).aspx.
2. You can declare compatible versions with binding redirection in the web.config. See this article: http://msdn.microsoft.com/en-us/library/433ysdt1.aspx.
In general the second approach is recommended because:
1. You cannot use partial reference to assemblies in the Global Assembly cache, meaning your control will throw the same exception if Sitefinity assemblies are in the GAC.
2. You explicitly state compatible versions.