This section is riddled with code examples and usages for extending the functionality of Umbraco. The examples include the following:
Remember to check out the download section of the Wrox website for all these code samples.
Clearly you can do a lot more in terms of making Umbraco a fully fledged custom application, but these samples should get you going.
The downloadable Visual Studio project accompanying this book named UmbUsersGuide.Samples is a solid representation of what your project template should look like. It includes the standard folder structure, all required references to Umbraco libraries, and sample build events to automatically deploy the project DLLs and other files to your website installation.
The List Subpages from Current Page macro recreates the same output as one of the existing XSLT templates that you looked at in Chapter 5. See Figure 12-1 for a view of the expected output.
To create your new user control, simply follow these steps:
The code in Listing 12-1 uses the Umbraco NodeFactory. This class allows you to programmatically query the XML cache, just like discussed in Chapter 11 using XSLT. It's fast, read-only, doesn't make any trips to the database, is ideal for this example, and should always be used when presenting data from the content tree in your .NET code. Later, this chapter covers the Document API, which allows for CRUD operations and interaction with the underlying database.
LISTING 12-1: ListSubPagesFromCurrentPage.ascx.cs
using System; using System.Text; using umbraco; using umbraco.presentation.nodeFactory; namespace UmbUsersGuide.Samples.UserControls { public partial class ListSubPagesFromCurrentPage : System.Web.UI.UserControl { protected void Page_Load(object sender, EventArgs e) { // Get the current node that we are on // reflects the node where this control is currently // included. var n = Node.GetCurrent(); // start html string for output var htmlOut = new StringBuilder(); htmlOut.Append(“<ul>”); // Loop over the nodes children and generate // some HTML to present to the view var childNodes = n.Children; foreach (Node childNode in childNodes) { htmlOut.Append(“<li>”); htmlOut.AppendFormat(“<a href=”{0}”>{1}</a>”, library.NiceUrl(childNode.Id), childNode.Name); htmlOut.Append(“</li>”); } // finish the html string builder htmlOut.Append(“</ul>”); // Render the HTML in our output panel. _outputPanel.Text = htmlOut.ToString(); } } }
LISTING 12-2: ListSubPagesFromCurrentPage.ascx
<%@ Control Language=“C#” AutoEventWireup=“true” CodeBehind=“ListSubPagesFromCurrentPage.ascx.cs” Inherits=“UmbUsersGuide.Samples.UserControls.ListSubPagesFromCurrentPage” %> <asp:Literal ID=“_outputPanel” runat=“server”></asp:Literal>
Insert the macro in the Runway master template, and you should see a standard unordered HTML list with the children of the current page nicely linked using the SEO-friendly URLs generated by the Umbraco library NiceUrl method.
So, you have created some document types that allow your users to work with products in the content tree. One of the requirements for displaying the products is to output all the products and their properties in a sortable table. If you have ever worked with a .NET GridView control you already know that it allows for automatic sorting and paging as long as you provide the data in one of the following ADO.NET formats:
Umbraco allows you to render the nodes as a DataTable very easily. See Listing 12-3 for an example of returning nodes in a DataTable type. The corresponding view is shown in Listing 12-4.
LISTING 12-3: ListPagesAsGridView.ascx.cs
using System; using umbraco.presentation.nodeFactory; namespace UmbUsersGuide.Samples.UserControls { public partial class ListPagesAsGridView : System.Web.UI.UserControl { protected void Page_Load(object sender, EventArgs e) { // Get the current node that we are on // reflects the node where this control is currently // included. var n = Node.GetCurrent(); // Simply set the data source for the GridView // and data bind it. _nodeGrid.DataSource = n.ChildrenAsTable(); _nodeGrid.DataBind(); } } }
LISTING 12-4: ListPagesAsGridView.ascx
<%@ Control Language=“C#” AutoEventWireup=“true” CodeBehind=“ListPagesAsGridView.ascx.cs” Inherits=“UmbUsersGuide.Samples.UserControls.ListPagesAsGridView” %> <asp:GridView ID=“_nodeGrid” runat=“server”> </asp:GridView>
If you receive a system exception (System.Web.HttpException) when running your newly created macro, the reason is most likely because you are missing the <form id=“1” runat=“server” /> tag in your master page, or you have placed the macro outside of the existing form tag.
Running this code renders all the properties of the returned nodes. You can, of course, further refine this output by working with all the features of the .NET GridView control (such as sorting, paging, DataColumns, and so on).
Programmatically adding content to the tree from user input will sometimes be necessary. A great example of this is if your site supports a knowledgebase or frequently asked questions (FAQ). So, you need to add a form to one of your pages that takes the user's input, maps the various fields to predefined document type properties, and saves the document. You build on this example over the next few sections.
You must first create a new document type (discussed in Chapter 3) so that you have something to work with, as shown in Figure 12-5 and in the following steps.
LISTING 12-5: CreateFAQ.ascx.cs
using System; using umbraco.BusinessLogic; using umbraco.cms.businesslogic.web; namespace UmbUsersGuide.Samples.UserControls { public partial class CreateFAQ : System.Web.UI.UserControl { protected void Page_Load(object sender, EventArgs e) { } protected void _save_Click(object sender, EventArgs e) { // need to register a user to associate the newly created // document with. Since this is an anonymous user, we'll // use the admin account which always has an ID of 0 User adminUser = new User(0); // create a new document using the Umbraco Document API Document faq = Document.MakeNew( _questionTitle.Text, // this will be the nodeName DocumentType.GetByAlias(“faq”), // get the document type ID by the alias we created adminUser, 1055); // the parent ID where this new page should live (the FAQ Area) // inject the user input into the document type properties // of the FAQ node that was just created above faq.getProperty(“question”).Value = _question.Text; faq.getProperty(“author”).Value = _yourName.Text; // We could optionally choose to publish the node here // In this case we're not going to as we don't want // unreviewd questions to appear on the site. //faq.Publish(adminUser); // This adds the created and published document to // the xml cache //umbraco.library.UpdateDocumentCache(faq.Id); // Redirect the user to a Thank You page that we created // in the content tree. We use Umbraco's NiceUrl helper // method to generate the URL by node id Response.Redirect(umbraco.library.NiceUrl(1056), true); } } }
<%@ Control Language=“C#” AutoEventWireup=“true” CodeBehind=“CreateFAQ.ascx.cs” Inherits=“UmbUsersGuide.Samples.UserControls.CreateFAQ” %> <fieldset> <label>Your Name</label> <asp:TextBox ID=“_yourName” runat=“server” /> <br /> <label>Question Title</label> <asp:TextBox ID=“_questionTitle” runat=“server” /> <br /> <label>Question</label> <asp:TextBox ID=“_question” TextMode=“MultiLine” Columns=“30” Rows=“10” runat=“server” /> <br /> <asp:Button ID=“_save” Text=“Submit Question” runat=“server” onclick=“_save_Click” /> </fieldset>
In Listing 12-5 you may notice some hardcoded IDs for the parent page as well as the redirect page. This is not ideal as you must recompile the code if any of the nodes change. The section “Passing Data to .NET Through Macro Parameters” details how to add public properties and pass in data as macro parameters to .NET controls.
Notice here that you have, by not publishing the programmatically created node, added a small workflow. An editor or administrator will have to review the question, provide an answer, and then publish the FAQ entry to make it public.
Umbraco ships with a lot of different data types and in most cases they are sufficient for standard content management fields. However, in some cases, like the FAQ example that you have been working with, a custom field may be required. Take for instance the category field that you added to the FAQ document type. As it stands now, the editor is forced to type in a value. That is not ideal because typos can be made and typically you want categories to remain standard across all the entries. A better solution might be to query for a set of categories that are stored in a custom table and outside of the Umbraco framework.
This next example shows you just how to do that. Here's the gist of what will go down:
To set a manual list of categories, follow these steps.
LISTING 12-7: CategoriesDT.ascx.cs
using System; using System.Data; using umbraco.editorControls.userControlGrapper; namespace UmbUsersGuide.Samples.UserControls { public partial class CategoriesDT : System.Web.UI.UserControl, IUsercontrolDataEditor { public string UmbracoValue = “”; protected void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) { // Wire up the dropdownlist and load the // data items _categories.DataSource = GetCategories(); _categories.DataTextField = “Name”; _categories.DataValueField = “ProductID”; _categories.DataBind(); } // if this is a postback, set the UmbracoValue // public property so we can reference the value // in the implemented interface below else if (Page.IsPostBack) { UmbracoValue = _categories.SelectedValue; } // If there is a saved value, always set it as the // [selected] value of the control if (!UmbracoValue.Equals(“”)) { _categories.Items.FindByValue(UmbracoValue).Selected = true; } } private DataTable GetCategories() { // Setup the data table columns so tha we can // populate them below. DataTable dt = new DataTable(); dt.Columns.Add(“ProductID”, Type.GetType(“System.Int32”)); dt.Columns.Add(“Name”, Type.GetType(“System.String”)); // populate with static data DataRow dr = dt.NewRow(); dr[“ProductID”] = 1; dr[“Name”] = “Technical Questions”; dt.Rows.Add(dr); dr = dt.NewRow(); dr[“ProductID”] = 2; dr[“Name”] = “Backoffice Interface”; dt.Rows.Add(dr); dr = dt.NewRow(); dr[“ProductID”] = 3; dr[“Name”] = “Permissions”; dt.Rows.Add(dr); dr = dt.NewRow(); dr[“ProductID”] = 4; dr[“Name”] = “Custom Functionality”; dt.Rows.Add(dr); return dt; } #region IUsercontrolDataEditor Members public object value { get { // if there is no value, set the value to null // otherwise use the selected value return UmbracoValue; } set { // When the control is loaded in the backoffice, // check if there is a saved value. If so, set the // matching item in the dropdownlist to selected. if (value != null && !String.IsNullOrEmpty(value.ToString())) { UmbracoValue = value.ToString(); } } } #endregion } }
LISTING 12-8: CategoriesDT.ascx
<%@ Control Language=“C#” AutoEventWireup=“true” CodeBehind=“CategoriesDT.ascx.cs” Inherits=“UmbUsersGuide.Samples.UserControls.CategoriesDT” %> <asp:DropDownList ID=“_categories” runat=“server”> </asp:DropDownList>
All that is happening in Listing 12-7 is that the public object is getting the value and performing a save to the document type property (in this case, category). Then upon loading of this custom data type, the public object value is returned and matched to an existing item in the dropdown (the list of items from the database).
The only thing left to do is add the CategoriesDT user control as a data type in the backoffice and switch out the document type control with the new data type.
If you change the Database data type after values have already been saved using the data type, you will lose all the saved data. So, choose carefully when creating your new data types.
As always with Umbraco multiple solutions exist. Other options to make this data type even more flexible in terms of data source would be:
In fact if you ask ten Umbraco developers how they would go about solving this problem, you would probably end up with at least eight different answers.
This section's example is a very popular feature on most websites. It's a simple contact form that allows you to send an email to a specified email address with the contents of the form. As usual, Umbraco leverages the standard .NET mail classes and settings to accomplish this. It even comes with a handy library method called SendMail that you can use to send simple emails (as shown by the bolded code in Listing 12-9). All you need to do is set up an email server in the Web.config. The default installation is configured for localhost, as shown in the following.
<system.net> <mailSettings> <smtp> <network host=“127.0.0.1” userName=“username” password=“password” /> </smtp> </mailSettings> </system.net>
To configure Umbraco to use your own SMTP (simple mail transport protocol) server, follow these steps:
For testing purposes in your development environment, you can configure the SMTP service with the following settings. This will simply write the results to a local file as specified in the pickupDirectoryLocation path.
<system.net> <mailSettings> <smtp deliveryMethod=“SpecifiedPickupDirectory” > <specifiedPickupDirectory pickupDirectoryLocation=“C: estdropfolder”/> </smtp> </mailSettings> </system.net>
All you have to do is set it to an SMTP server that you have access to and authority to relay through. Without further ado, see Listings 12-9 and 12-10 for this example.
LISTING 12-9: ContactForm.ascx.cs
using System; namespace UmbUsersGuide.Samples.UserControls { public partial class ContactForm : System.Web.UI.UserControl { protected void Page_Load(object sender, EventArgs e) { if (Page.IsPostBack) { // hide the form _formView.Visible = false; // Show the Thank You Message _messageView.Visible = true; } } protected void _sendEmail_Click(object sender, EventArgs e) { // send an email using the Umbraco libray SendMail method umbraco.library.SendMail(_emailAddress.Text, “[email protected]”, “Someone filled out the contact form!”, _message.Text, true); } } }
LISTING 12-10: ContactForm.ascx
<%@ Control Language=“C#” AutoEventWireup=“true” CodeBehind=“ContactForm.ascx.cs” Inherits=“UmbUsersGuide.Samples.UserControls.ContactForm” %> <asp:Panel ID=“_formView” runat=“server”> <fieldset> <label>Your Name</label> <asp:TextBox ID=“_yourName” runat=“server” /> <br /> <label>Your Email Address</label> <asp:TextBox ID=“_emailAddress” runat=“server” /> <br /> <label>Your Phone Number</label> <asp:TextBox ID=“_phone” runat=“server” /> <br /> <label>Message</label> <asp:TextBox ID=“_message” TextMode=“MultiLine” Columns=“30” Rows=“10” runat=“server” /> <br /> <asp:Button ID=“_sendEmail” Text=“Send Email” runat=“server” onclick=“_sendEmail_Click” /> </fieldset> </asp:Panel> <asp:Panel ID=“_messageView” Visible=“false” runat=“server”> </asp:Panel>
Throughout all the examples so far you have probably noticed that a lot of hard-coding values have taken place. For example, when creating a document, wouldn't it be nice to pass in the ParentId of the node where you want to create the FAQs? Or, in the contact form, wouldn't it be great if the To Email address could be set dynamically, allowing you to reuse the form in multiple places for multiple reasons?
Chapter 5 discusses macro parameters and how to use them. Well, you can use these macro parameters to pass data to your .NET user controls via public properties.
To demonstrate, let's modify the preceding example where the FAQ question was created, and pass in the ParentId and the ThankYouId using macro parameters. The bolded code in Listing 12-11 shows the added or changed code.
LISTING 12-11: CreateFAQ.ascx.cs
using System; using umbraco.BusinessLogic; using umbraco.cms.businesslogic.web; namespace UmbUsersGuide.Samples.UserControls { public partial class CreateFAQ : System.Web.UI.UserControl { public int ParentId { get; set; } public int ThankYouId { get; set; } protected void Page_Load(object sender, EventArgs e) { } protected void _save_Click(object sender, EventArgs e) { // need to register a user to associate the newly created // document with. Since this is an anonymous user, we'll // use the admin account which always has an ID of 0 User adminUser = new User(0); // create a new document using the Umbraco Document API Document faq = Document.MakeNew( _questionTitle.Text, // this will be the nodeName DocumentType.GetByAlias(“faq”), // get the document type ID by the alias we created adminUser, ParentId); // the parent ID where this new page should live (the FAQ Area) // inject the user input into the document type properties // of the FAQ node that was just created above faq.getProperty(“question”).Value = _question.Text; faq.getProperty(“author”).Value = _yourName.Text; // We could optionally choose to publish the node here // In this case we're not going to as we don't want // unreviewed questions to appear on the site. //faq.Publish(adminUser); // This adds the created and published document to // the xml cache //umbraco.library.UpdateDocumentCache(faq.Id); // Redirect the user to a Thank You page that we created // in the content tree. We use Umbraco's NiceUrl helper // method to generate the URL by node id Response.Redirect(umbraco.library.NiceUrl(ThankYouId), true); } } }
When you have built the altered user control and deployed it to your Umbraco installation, adding these public properties as Umbraco macro parameters is a breeze:
Now, when you go to re-add the FAQ form to the FAQ Area page, you will be prompted to select the corresponding Parent and Thank You pages, as shown in Figure 12-13.
You may also want to change the Name attribute of the parameter to be more user friendly, but this is completely optional from a functional standpoint.
The most obvious benefit of working with public properties and macro parameters is that you have now removed the requirements of code changes if you need to update where the submitted FAQs should be saved or what page to redirect the user to upon submission.
If you must deal with personalized or restricted content in your website, you need the ability to have users log in by providing member account credentials. As mentioned earlier in this book, Umbraco implements the standard .NET Membership Provider model. This means that you can pretty much just drag and drop the various login controls onto your user controls and be done! Well, at least theoretically. You need to perform a few configurations to get this working.
Chapter 2 provides details on how to work with members—adding member types, member groups, and the member details. To get the standard Umbraco Membership Provider to work, only two things need to happen:
You can find the membership providers configuration in the <system.web><membership /></system.web> node in Web.config.
That's it! Now, all you need to do is add an <asp:Login /> control to a user control and create a macro for it. See Listings 12-12 and 12-13 for an example (be aware that it is alarmingly simple).
using System; namespace UmbUsersGuide.Samples.UserControls { public partial class Login : System.Web.UI.UserControl { // The default redirect page after logging in public int LoginRedirectPage { get; set; } protected override void OnInit(EventArgs e) { base.OnInit(e); _loginForm.LoggedIn += LoginUser_LoggedIn; } // When the user has successfully authenticated // redirect them to the specified page void LoginUser_LoggedIn(object sender, EventArgs e) { // Redirect the user to a specified page Response.Redirect(umbraco.library.NiceUrl(LoginRedirectPage)); } protected void Page_Load(object sender, EventArgs e) { } } }