Did you know squirrels love eating bananas? Neither did I until yesterday when I was enlightened by this tweet. Immediately, I knew I had to carve out a corner in the banana-loving-rodent market.
What better way to acquire leads than by offering free stuff after signing up for an account? I just needed an unsquirrely way to save answers to questions on my signup form to help me better understand my prospective customers.
I decided to take advantage of Sitecore’s ability to save custom information on user profiles but wanted to avoid passing strings directly to my users’ Sitecore.Security.UserProfile objects — saving lots of information in a string can be cumbersome if not downright messy.
Under the same JSON theme of the Sitecore Item Web API, I decided to build a wrapper class around the UserProfile object that uses an utility object to serialize/deserialize objects to/from strings of JSON before saving/retrieving.
Knowing the Sitecore Item Web API converts objects into JSON, I poked around Sitecore.ItemWebApi.dll (v1.0.0 rev. 120828) using .NET Reflector in search of a serializer I could use in my solution. As I expected, I found a class that serializes objects into strings of JSON.
However, to my dismay, the same object does not deserialize strings back into objects:
Further, I found no classes within Sitecore.ItemWebApi.dll that deserialize JSON strings. This pretty much meant I had to “roll my own”:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Sitecore.Sandbox.Utilities.Serialization.Base { public interface ISerializer { string Serialize(object objectToSerialize); T Deserialize<T>(string objectAsString); object Deserialize(string objectAsString, Type objectType); } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.Script.Serialization; using Sitecore.Sandbox.Utilities.Serialization.Base; namespace Sitecore.Sandbox.Utilities.Serialization { public class JsonSerializer : ISerializer { private static JavaScriptSerializer _javaScriptSerializer = new JavaScriptSerializer(); private JsonSerializer() { } public string Serialize(object objectToSerialize) { return _javaScriptSerializer.Serialize(objectToSerialize); } public T Deserialize<T>(string objectAsString) { return _javaScriptSerializer.Deserialize<T>(objectAsString); } public object Deserialize(string objectAsString, Type objectType) { return _javaScriptSerializer.Deserialize(objectAsString, objectType); } public static ISerializer CreateNewJsonSerializer() { return new JsonSerializer(); } } }
The JsonSerializer class above not only gives client-code the ability to serialize objects into JSON strings, but also offers methods for deserialization.
Plus, unlike the JsonSerializer class in Sitecore.ItemWebApi.Serialization, this JsonSerializer class creates only one instance of a JavaScriptSerializer object across multiple instances of itself. There is no need to have multiple instances of this class floating around in memory since the underlying core logic for this class will not — and should not — change.
Next, I defined my UserProfile wrapper class:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Sitecore.Sandbox.Security.UserProfiles.Base { public interface IUserProfileTypedProperties { string FullName { get; set; } T GetCustomProperty<T>(string propertyName); void SetCustomProperty(string propertyName, object propertyValue); void Save(); } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Diagnostics; using Sitecore.Security; using Sitecore.Sandbox.Utilities.Serialization.Base; using Sitecore.Sandbox.Utilities.Serialization; using Sitecore.Sandbox.Security.UserProfiles.Base; namespace Sitecore.Sandbox.Security.UserProfiles { public class UserProfileTypedProperties : IUserProfileTypedProperties { private static ISerializer _defaultSerializer = GetNewDefaultSerializer(); private UserProfile UserProfile { get; set; } private ISerializer Serializer { get; set; } public string FullName { get { return UserProfile.FullName; } set { UserProfile.FullName = value; } } private UserProfileTypedProperties(UserProfile userProfile) :this(userProfile, _defaultSerializer) { } private UserProfileTypedProperties(UserProfile userProfile, ISerializer serializer) { SetUserProfile(userProfile); SetSerializer(serializer); } private void SetSerializer(ISerializer serializer) { Assert.ArgumentNotNull(serializer, "serializer"); Serializer = serializer; } private void SetUserProfile(UserProfile userProfile) { Assert.ArgumentNotNull(userProfile, "userProfile"); UserProfile = userProfile; } public T GetCustomProperty<T>(string propertyName) { string propertyValue = UserProfile.GetCustomProperty(propertyName); return Deserialize<T>(propertyValue); } private T Deserialize<T>(string objectAsString) { return Serializer.Deserialize<T>(objectAsString); } public void SetCustomProperty(string propertyName, object propertyValue) { string objectAsString = Serialize(propertyValue); SetCustomProperty(propertyName, objectAsString); } private void SetCustomProperty(string propertyName, string objectAsString) { UserProfile.SetCustomProperty(propertyName, objectAsString); } private string GetCustomProperty(string propertyName) { return UserProfile.GetCustomProperty(propertyName); } private string Serialize(object objectToSerialize) { return Serializer.Serialize(objectToSerialize); } public void Save() { UserProfile.Save(); } public static IUserProfileTypedProperties CreateNewUserProfileTypedProperties(UserProfile userProfile) { return new UserProfileTypedProperties(userProfile); } public static IUserProfileTypedProperties CreateNewUserProfileTypedProperties(UserProfile userProfile, ISerializer serializer) { return new UserProfileTypedProperties(userProfile, serializer); } private static ISerializer GetNewDefaultSerializer() { return JsonSerializer.CreateNewJsonSerializer(); } } }
The UserProfile wrapper above offers client-code the option of passing in their own serializers — as long as their serializers implement the ISerializer interface I defined above — or provides an instance of the JsonSerializer by default.
Now, let’s test all of this stuff above using a simple form in a sublayout for creating an account and saving/retrieving a typed object from a custom UserProfile property:
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="UserProfile Typed Property Test.ascx.cs" Inherits="Sitecore650rev120706.layouts.sublayouts.UserProfile_Typed_Property_Test" %> <style type="text/css"> .form-container { margin-left: 20px; } h1 { margin-top: 0; } .form { list-style: none; margin: 0; padding: 0; } .form li, td, .cover { zoom: 1; text-align: left; } .form li:after, td:after, .cover:after { content: "."; display: block; clear: both; visibility: hidden; line-height: 0; height: 0; } .form > li > div, input[type="radio"], input[type="radio"] + label, .float-left { float: left; } .form > li > div + div { margin-left: 5px; } .red { color: red; } .signup, .bold { font-weight: bold; } input[type="radio"] + label { display: block; width: 130px; } .signup, .float-right { float: right; } .signup { width: 100px; height: 50px; font-size: 20px; } </style> <div class="cover"> <asp:Image ID="imgSquirrel" CssClass="float-left" ImageUrl="/img/squirrel-banana.jpg" runat="server" /> <asp:MultiView ID="mvUserProfileInformation" ActiveViewIndex="0" runat="server"> <asp:View ID="vSignupForm" runat="server"> <div class="float-left form-container"> <h1> Hey Squirrels </h1> <h2 class="red"> Free Bananas When You Signup! </h2> <ul class="form"> <li> <div> <asp:Label ID="lblName" CssClass="bold" Text="Name:" AssociatedControlID="txtName" runat="server" /> <span class="red">*</span> </div> <div> <asp:TextBox ID="txtName" runat="server" /> <asp:RequiredFieldValidator ID="rfvName" ControlToValidate="txtName" ErrorMessage="<br />Please tell us your name!" CssClass="red" Display="Dynamic" ValidationGroup="SquirrelForm" runat="server" /> </div> </li> <li> <div> <asp:Label ID="lblUsername" CssClass="bold" Text="Username:" AssociatedControlID="txtName" runat="server" /> <span class="red">*</span> </div> <div> <asp:TextBox ID="txtUsername" runat="server" /> <asp:RequiredFieldValidator ID="rfvUsername" ControlToValidate="txtUsername" ErrorMessage="<br />Please enter a username!" CssClass="red" Display="Dynamic" ValidationGroup="SquirrelForm" runat="server" /> </div> </li> <li> <div> <asp:Label ID="lblPassword" CssClass="bold" Text="Password:" AssociatedControlID="txtPassword" runat="server" /> <span class="red">*</span> </div> <div> <asp:TextBox ID="txtPassword" TextMode="Password" runat="server" /> <asp:RequiredFieldValidator ID="rfvPassword" ControlToValidate="txtPassword" ErrorMessage="<br />Choose a password!" CssClass="red" Display="Dynamic" ValidationGroup="SquirrelForm" runat="server" /> </div> </li> <li> <div> <asp:Label ID="lblPasswordConfirm" CssClass="bold" Text="Confirm Password:" AssociatedControlID="txtPasswordConfirm" runat="server" /> <span class="red">*</span> </div> <div> <asp:TextBox ID="txtPasswordConfirm" TextMode="Password" runat="server" /> <asp:RequiredFieldValidator ID="rfvPasswordConfirm" ControlToValidate="txtPasswordConfirm" ErrorMessage="<br />Please confirm your password!" CssClass="red" Display="Dynamic" ValidationGroup="SquirrelForm" runat="server" /> <asp:CompareValidator ID="cvPasswordConfirm" ControlToValidate="txtPasswordConfirm" ControlToCompare="txtPassword" ErrorMessage="<br />Passwords don't match!" CssClass="red" Display="Dynamic" ValidationGroup="SquirrelForm" runat="server" /> </div> </li> <li> <div> <asp:Label ID="lblWhereLive" CssClass="bold" Text="You live (choose one):" AssociatedControlID="cblFoods" runat="server" /> <span class="red">*</span> </div> <div> <asp:RadioButtonList ID="rblWhereLive" runat="server"> <asp:ListItem Text="Up a tree" Value="Up a tree" /> <asp:ListItem Text="In an attic" Value="In an attic" /> <asp:ListItem Text="In a broken-down car up on cinder blocks" Value="In a broken-down car up on cinder blocks" /> </asp:RadioButtonList> <asp:RequiredFieldValidator ID="rfvWhereLive" ControlToValidate="rblWhereLive" ErrorMessage="<br />Please choose one domicile!" CssClass="red" Display="Dynamic" ValidationGroup="SquirrelForm" runat="server" /> </div> </li> <li> <div> <asp:Label ID="lblYouThinkYouARodent" CssClass="bold" Text="Do you think you're a rodent?:" AssociatedControlID="rblYouARodent" runat="server" /> </div> <div> <asp:RadioButtonList ID="rblYouARodent" runat="server"> <asp:ListItem Text="Yes" Value="Yes" Selected="true" /> <asp:ListItem Text="No" Value="No" /> </asp:RadioButtonList> </div> </li> <li> <div> <asp:Label ID="lblFoodPreference" CssClass="bold" Text="I like to eat (check all that apply):" AssociatedControlID="cblFoods" runat="server" /> </div> <div> <asp:CheckBoxList ID="cblFoods" runat="server"> <asp:ListItem Text="Acorns" Value="Acorns" /> <asp:ListItem Text="Walnuts" Value="Walnuts" /> <asp:ListItem Text="Chestnuts" Value="Chestnuts" /> <asp:ListItem Text="Bananas" Value="Bananas" /> </asp:CheckBoxList> </div> </li> </ul> <asp:Button ID="btnSignup" CssClass="signup" OnClick="btnSignup_Click" Text="Signup" ValidationGroup="SquirrelForm" runat="server" /> </div> </asp:View> <asp:View ID="vUserProfileSavedInformation" runat="server"> <div class="float-left form-container" style="width: 330px;"> <h1> Your information: </h1> <div> <asp:Label ID="lblYourName" CssClass="bold" Text="Name:" runat="server" /> <asp:Literal ID="litYourName" runat="server" /> </div> <div> <asp:Label ID="lblYourUsername" CssClass="bold" Text="Username:" runat="server" /> <asp:Literal ID="litYourUsername" runat="server" /> </div> <div> <asp:Label ID="lblYouARodent" CssClass="bold" Text="Do you think you're a rodent?:" runat="server" /> <asp:Literal ID="litYouARodent" runat="server" /> </div> <div> <asp:Label ID="lblYourDomicile" CssClass="bold" Text="You live:" runat="server" /> <asp:Literal ID="litWhereLive" runat="server" /> </div> <div> <asp:Label ID="lblYouLikeToEat" CssClass="bold" Text="You like to eat:" runat="server" /> <asp:Literal ID="litFoodPreference" runat="server" /> </div> </div> </asp:View> </asp:MultiView> </div>
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using Sitecore.Security; using Sitecore.Security.Accounts; using Sitecore.Security.Authentication; using Sitecore.Sandbox.Security.UserProfiles; using Sitecore.Sandbox.Security.UserProfiles.Base; namespace Sitecore650rev120706.layouts.sublayouts { public partial class UserProfile_Typed_Property_Test : System.Web.UI.UserControl { private const string SquirrelInformationPropertyName = "Squirrel Information"; protected void Page_Load(object sender, EventArgs e) { } protected void btnSignup_Click(object sender, EventArgs e) { if (Page.IsValid) { string username = GetUsername(); User user = CreateUser(username, txtPassword.Text); if(Login(user, txtPassword.Text)) { SaveSquirrelInformation(); ShowSquirrelInformation(); } } } private User CreateUser(string username, string password) { return User.Create(username, txtPassword.Text); } private string GetUsername() { const string domain = "extranet"; if(!string.IsNullOrEmpty(txtUsername.Text)) return string.Concat(domain, "\\", txtUsername.Text); return string.Empty; } private bool Login(User user, string password) { if(user != null && user.Profile != null) return AuthenticationManager.Login(user.Profile.UserName, password, false); return false; } private void SaveSquirrelInformation() { SquirrelInformation squirrelInformation = GetSquirrelInformationFromForm(); IUserProfileTypedProperties userProfileTypedProperties = UserProfileTypedProperties.CreateNewUserProfileTypedProperties(Sitecore.Context.User.Profile); userProfileTypedProperties.FullName = txtName.Text; userProfileTypedProperties.SetCustomProperty(SquirrelInformationPropertyName, squirrelInformation); userProfileTypedProperties.Save(); } private SquirrelInformation GetSquirrelInformationFromForm() { return new SquirrelInformation { Domicile = rblWhereLive.SelectedValue, YouARodent = rblYouARodent.Text, FoodPreferences = GetFoodPreferencesFromForm() }; } private List<string> GetFoodPreferencesFromForm() { List<string> foodPreferences = new List<string>(); foreach (ListItem listItem in cblFoods.Items) { if (listItem.Selected) foodPreferences.Add(listItem.Value); } return foodPreferences; } private void ShowSquirrelInformation() { UserProfile userProfile = Sitecore.Context.User.Profile; litYourName.Text = userProfile.FullName; litYourUsername.Text = userProfile.UserName; SquirrelInformation squirrelInformation = GetSavedSquirrelInformation(userProfile); litWhereLive.Text = squirrelInformation.Domicile; litYouARodent.Text = squirrelInformation.YouARodent; litFoodPreference.Text = string.Join(", ", squirrelInformation.FoodPreferences); mvUserProfileInformation.SetActiveView(vUserProfileSavedInformation); } private static SquirrelInformation GetSavedSquirrelInformation(UserProfile userProfile) { IUserProfileTypedProperties userProfileTypedProperties = UserProfileTypedProperties.CreateNewUserProfileTypedProperties(userProfile); return userProfileTypedProperties.GetCustomProperty<SquirrelInformation>(SquirrelInformationPropertyName); } private class SquirrelInformation { public string Domicile { get; set; } public string YouARodent { get; set; } public List<string> FoodPreferences { get; set; } } } }
The form:
After creating an account, I just show a subset of the information the squirrel provided on the form:
Just to let you know, the logic in sublayout’s code-behind is not very robust — it throws an exception when users refresh the “confirmation” page after creating an account (I’m not checking to see if the user had already created an account and is logged in).
Plus, it’s in need of refactoring — I should move the SquirrelInformation class into its own file.
I’m not even going to mention how bad the front-end code needs some attention — my front-end friends are probably ashamed they even know me and are no doubt thinking I’ve gone completely bananas. They may be right. 🙂
I promise I’ll go back and clean it up. I just needed to get it out the door. Winter is quickly approaching and I need to market my bananas as soon as possible. I need to reap in some dough to buy and bury more acorns.
That’s it for now. I have to go climb a tree. Talk to you later 🙂