Linakis Digital

Ideas, Thoughts, Inspiration

The new Sitecore Email Experience Manager is out and it ROCKS!

Sitecore Email Experience Manager (EXM) is Sitecore's Email campaigning module. It is however, a whole lot more than a “Newsletter Engine” as one may mistake it for. With the Sitecore Email Experience Manager, you can use everything you know about your customers to personalize every aspect of your emails, so you always send just the right email at just the right time to keep the cross-channel conversation moving forward.

Sitecore maintains an up-to-date profile and cross channel history for each unique customer. With that real-time, contextual information, you can use email with precision as part of a completely connected and personalized customer experience.

Sitecore EXM lets you schedule and trigger a single email or a series of emails based on demographics, real-time behaviors, events, or any other customer experience data that’s available. From welcome, birthday, and loyalty programs to newsletters and thank you notes, you can nurture customer relationships automatically, and make email an integral and relevant part of the cross channel conversation.

It makes it easy to run A/B and multivariate testing on any element of your emails, including subject lines, messaging, images, format, and timing. Make your next interactions—in any channel—that much better by learning what works best for each customer or segment.

Lately Sitecore published the new EXM Version 3.4 which has some major enhancements over previous versions. Some of these are:
  • The new Sitecore Email Cloud service. A cloud service coupled to Sitecore EXM that is responsible for delivering emails to the customers inbox. It is a cloud service delivered by Sitecore.
  • Reporting Enhancements. Sitecore’s advanced reporting and analytics help you measure the engagement value of your emails within the context of the complete customer experience, so you can compare content, campaigns, and channels objectively. The new Reporting adds new charts to the dashboard and campaign report.
  • Redesigned Recipient & Subscriber management process
If you would like to see the power of Sitecore EXM and what it can do for your business please contact us for a live Demo

Activate one validator at a time in a webform

Recently we had a WFFM form that had 3 separate validators running on the same field - a password field with 3 different regular expression validators, one for length and two for other password policy rules.

The issue with the implementation was that for some invalid test inputs, at least two of the validators would kick in simultaneously, which was not anticipated by the website design (the design allowed only one validator active at any given point).

My solution was to tap into the ValidatorUpdateDisplay that is defined in one of the ASP.NET javascript resources and simply override it. To do that, I first recognized that functions in javascript are object like, so first of all, I assigned the function to my own variable:

var vold = ValidatorUpdateDisplay;


Then I changed the original to cater for my needs making sure I called the original at the end:

var ValidatorUpdateDisplay = function (validator) {
    if (!validator.isvalid) {
        for (i = 0; i < Page_Validators.length; i++) {
            if (Page_Validators[i].controltovalidate == validator.controltovalidate && !Page_Validators[i].isvalid && Page_Validators[i].id != validator.id) {
                validator.isvalid = true;
            }
        }
    }
    vold(validator);
}

So now, only one invalid validator per field is actually "active" at any given time.

Apparently, this approach is valid outside the boundaries of WFFM as well, any old webform can benefit from this.

Happy coding!


How great Customer Experience (CX) drives revenues

Customer experience is the single most important determinant of business success. This is something that we have been hearing about for decades, in everything from Harvard Business Review articles to Forrester reports. However, none of these studies include hard numbers on the benefits that could be derived from well-orchestrated CX strategies; hard details are few and far between.

Sitecore, which is a leader in digital marketing technology and in digital marketing solutions that transform customer experiences decided to research the quantifiable benefits of a customer experience strategy. They hired Vanson Bourne, together with Avanade, to conduct a global survey, and the results are truly mind-blowing!

The study, conducted with 880 decision makers in six countries, found that for every dollar invested in improving the customer experience, businesses generated three dollars in return. Brands investing in customer experience have seen a 19% increase in revenue in the past 12 months.

Some of the core findings are:

Huge benefits have been seen as a result of focusing on the customer experience

  • US $3 return on investment expected for every $1 invested in the customer experience
  • Almost six in ten (58%) have seen increased customer satisfaction over the last 12 months
  • Close to four in ten (37%) have seen improved sales cycles
  • So many more benefits have been realized, and are expected in the near future

Keeping up with competition is driving the customer experience, more than the customers themselves

  • 64% report that competition is a key driver, compared to 52% who say this is customer feedback
  • Not many organizations are able to get in front of their customers wants and needs
  • Only 14% admit that they are one step ahead of their customers’ changing expectations

Investment in technology, skills and people is how organizations are achieving benefits

  • 78% of organizations are achieving benefits by investing in modern marketing technology
  • Half (50%) do so by investing in skills and third-party expertise

Customer experience management is key

  • Eight in ten (80%) consider it important to see a return on marketing investment
  • 77% believe customer experience is important for delivering a strong, relevant customer experience
  • 68% are investing in it in the next year Lifetime customers are of great value, but organizations need to make them feel appreciated
  • 37% would compare their lifetime customers to diamonds and 46% to platinum
  • Feeling undervalued is the most likely reason a customer would feel frustrated with a brand

You may download the full report here

Linakis Digital is a Sitecore Gold Implementation Partner and provides a unique set of CX services designed to drive results. Please feel free to contact us and explore how we can work together in order to achieve better CX for your customers.

Add SOAP WSSE headers, the WCF way

Recently, I've been tasked to migrate a set of web service endpoints to new url's. That's all fine and dandy, however the new web services have WSSE security headers, while the old ones didn't. A quick search on the internet got me a lot of links about adding WSSE for Web References (i.e. "asmx" technology), but very little about Service References (i.e. "WCF" technology). In actual fact, it's probably because it's so easy to add the headers. Simply pop the extra headers in the client configuration:

  <endpoint address="https://an.awsome.service/definition.svc"
    binding="basicHttpBinding" bindingConfiguration="definition"
    contract="an.awsome.service.definition"
    name="an_awsome_service_definition" >
    <headers>
      <wsse:Security xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/07/secext">
        <wsse:UsernameToken xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility">
          <wsse:Username>TheUsername</wsse:Username>
          <wsse:SessionType xmlns="http://schemas.xmlsoap.org/ws/2002/07/secext">Stateless</wsse:SessionType>
          <wsse:Password Type="wsse:PasswordText">ThePassword</wsse:Password>
        </wsse:UsernameToken>
      </wsse:Security>
    </headers>
  </endpoint>
And you're good to go!

WFFM Save actions and the Sitecore factory

If you open up most of the default WFFM save actions in Sitecore, you'll notice that they don't have anything filled in the Assembly and Class fields, but have some sort of xpath in field Factory Object name

This Factory is a DI container that Sitecore has - I haven't delved deep into it yet, but now that I've realized the it's there and part of how it works I certainly plan to.

My reason for looking into this is pretty simple. There is a very nice article about shoehorning values into the AdaptedResultList object that is passed into a Save Action. I wanted to use that concept while subclassing the Send Message action. The reason is that I wanted to have a contact form, with a dropdown list where there were various "departments" that the user could choose to send the contact request to. 

Pretty standard stuff, you'd say. Not so much though if you want to use the standard Send Mail Message action to send the email to different recipients (or from different senders) depending on the "department" chosen, particularly if you'd prefer for security (see "email harvesting") reasons not to expose those email addresses in the HTML source code of the final contact page. Doing so, you no longer have the desired email in a value in the AdaptedResultList object, so bye-bye standard Send Mail save action, Hello Custom Code!

So you need to subclass the Sitecore.WFFM.Actions.SaveActions.SendMessage action. But here's the catch, once you do it, you get an error about two properties of the base class that are not instantiated properly. Then you go into ilSpy, and realize that this save action has a non-parameterless constructor where these two properties are passed in as parameters, and with an Interface type at that:

 public SendMessage(ISettings settings, IMailSender mailSender);

That's dependency injection right there if I ever saw one.

The sitecore documentation about custom save actions says nothing about this. The example code used creates a new save action from scratch.


"But I don't want to re-invent the wheel!" I can hear you cry out in frustration. Well, at least I did. After many fruitless hours of searching for any documentation or hints about the DI container and scheme used in Sitecore, I gave up, and practically copy-pasted the decompiled code from the SendMessage action into my own.And when that didn't work, I also went on and added the decompiled code of the "SendMessage" pipeline processors into my class. And then I got stuck at a particular point where I had to get an instance of 

Sitecore.Forms.Core.Dependencies.DefaultImplMailSender

to push into the 

private readonly IMailSender mailSender;
property of the action.

I checked, and saw I could go parameterless, so I did: 

mailSender = new Sitecore.Forms.Core.Dependencies.DefaultImplMailSender();
And got the dreaded warning:
'DefaultImplMailSender.DefaultImplMailSender()' is obsolete
The code worked, but it's definitely not future proof, and this type of contact form is popping up time and again, so I want this save action as future proof as possible.

So back to square one-ish. Or is it? In a rare moment of clarity, I searched the configuration for the actual xpath (the "Factory Object Name"), and for parts of it, and lo! and behold! In a file unsurprisingly named "Sitecore.WFFM.Dependencies.config" in App_config/Include folder, I found the following:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <wffm>
...
      <!--Save actions constructor configuration-->
      <actions>
...
        <sendMessage type="Sitecore.WFFM.Actions.SaveActions.SendMessage, Sitecore.WFFM.Actions">
          <param name="settings" ref="/sitecore/wffm/settings" />
          <param name="mailSender" ref="/sitecore/wffm/mailSender" />
        </sendMessage>
...
 </actions>
...
    </wffm>
  </sitecore>
</configuration>

there it was! /wffm/actions/sendMessage, just as the Factory Object name said it would be. And with the fully qualified types for the two arguments of the constructor as well!

I simply added the following within the actions

<SendEmailTest type="MyCustomNamespace.SendEmailTest, MyAssembly">
          <param name="settings" ref="/sitecore/wffm/settings" />
          <param name="mailSender" ref="/sitecore/wffm/mailSender" />
</SendEmailTest>

And just coded this as proof of concept:

    public class SendEmailTest: SendMessage
    {
        public SendEmailTest(ISettings settings, IMailSender mailSender) : base(settings, mailSender) { }
        public override void Execute(ID formId, AdaptedResultList adaptedFields, ActionCallContext actionCallContext = null, params object[] data)
        {
            Sitecore.Diagnostics.Log.Info("enter the test execute", this);
            base.Execute(formId, adaptedFields, actionCallContext, data);
        }
    }

Then went on to duplicate item /sitecore/system/Modules/Web Forms for Marketers/Settings/Actions/Save Actions/Send Email Message, renamed it into Send Mail Test (names don't really matter though) and changed the Factory Object name into the following:

/sitecore/wffm/actions/SendEmailTest

I added my new save action into a form and tested, and sure enough, this popped up in the log:

25072 17:30:14 INFO  enter the test execute

Isn't it nice when things just work? So now I was free to do whatever I wanted with the AdaptedResultList and then just call the base.Execute method with the altered one.

<imahappydev>Celebratory dance</imahappydev>


Happy coding!

Install Solr 6.1 on windows as a service

Intro

According to the official Solr documentation, since version 5.0, it is not a recommended practice to have Solr run in a 3rd party container (such as Tomcat). You can read more about it here and here.

Based on the official information on how to install and run Solr, you simply extract the zip in a folder and run the executable. However, that's not all of it, unfortunately. The issue is that when following these instructions, you simply run the program. Nothing gets installed as a service, so if the server gets rebooted, you need to re-run the executable.

Furthermore, the relevant documentation page only lists how to make Solr run as a service under Linux and Unix-based systems.


Solution

Here's a rundown of what I did to get things smoothly working. Most of this stuff is from http://coding-art.blogspot.gr/2016/07/running-solr-61-as-windows-service.html and from https://www.norconex.com/how-to-run-solr5-as-a-service-on-windows/

Downloads

  1. Latest Solr (at the time of writing, this was 6.1.0). From http://www.apache.org/dyn/closer.lua/lucene/solr/6.1.0 
  2. Latest Java Server JRE (at the time of writing, 1.8.102). From http://www.oracle.com/technetwork/java/javase/downloads/server-jre8-downloads-2133154.html. You will need to accept their License Agreement.
  3. Latest NSSM. From https://nssm.cc/download.


Installations

Java

The installation instructions for the Server JRE are located at http://docs.oracle.com/javase/8/docs/technotes/guides/install/windows_server_jre.html#CFHGHHFJ. Essentially, what they are saying is "extract the bundle and simply copy the files wherever you want". There are two issues with this: 
  • the .tar.gz extention is not recognized by default in Windows. If that's the case with you, you need to download and install a compression/decompression utility that can handle this extension, such as 7-zip. You can get it from here as a portable app.
  • After extraction, I opted to copy the resultant folder "jdk1.8.0_102" into "C:\Program Files\Java"

Solr

As per the documentation, I unzipped the zip file and copied everything over to C:\Program Files\Solr-6.1.0

NSSM

Similar to Solr, I unzipped the file and copied everything over to C:\Program Files\nssm

Configurations

Java

I had to go and register an environment variable, namely JAVA_HOME, with the value of the path to my java installation (i.e. C:\Program Files\Java\jdk1.8.0_102). To do that in Windows 2012 I went to "Computer"--> right click --> "Properties" --> "Advanced System Settings" --> "Environment Variables" button and clicked on the "New" button under "System Variables".
The above is a crucial step, as without it, when you attempt to run Solr, you'll get the message that "JAVA_HOME has not been set".

Solr

First of all, open a command prompt as Administrator, change to the bin folder within the Solr installation folder (mine was C:\Program Files\Solr-6.1.0\bin) and issue the command 

solr start
You should get something like the following:

C:\Program Files\solr-6.1.0\bin>solr start
Waiting up to 30 to see Solr running on port 8983
Started Solr server on port 8983. Happy searching!
C:\Program Files\solr-6.1.0\bin>

that's great. It's running, you can verify this by opening a browser and going to http://localhost:8983/solr. Stop the server by issuing 
solr stop -all

The Program Files folder is not a very handy location to have all the Solr Core definitions and data. So I decided to host this data within C:\Inetpub. I could equally go for C:\solr. In any case, copy the contents of the folder C:\Program Files\Solr-6.1.0\server\solr onto wherever you want to have the Solr files hosted. I went for C:\Inetpub\solr\solr-6.1.0-cores. The reason behind the extra "solr" folder will be explained in a bit.

Now in C:\Inetpub\solr I went and created a file called "solr_start.cmd". I opened it with notepad and wrote the following:

"C:\Program Files\solr-6.1.0\bin\solr" start -f -p 8983 -m 512m -s C:\inetpub\solr\solr-6.1-cores

the -f parameter means that Solr should run in the foreground. The -p sets the port, the -m sets the min/max amount of memory Solr is allowed to use and the -s tells the Solr server to look for cores (and create new ones) in that particular path.

Executing the solr_start.cmd, should get you a CLI with a lot of information, and no command prompt - the server is running in the foreground. If you open the Admin UI in a browser, you should be seeing the interface again, and also see an info message in the CLI that the admin interface has been accessed. Hit Ctrl-C in the window to stop the server (after a prompt to stop the batch job, where you should say "y" as in "yes").

You're now all set to configure the service via nssm.

NSSM

Open a command prompt as administrator and go to C:\Program Files\nssm\win64 (or win32 if you run a 32 bit system, which I doubt). From there, issue the following command:
nssm install "Apache Solr 6.1"
This will open up a GUI (after propmting for Admin approval) where you can fill in the pertinent information for the service you are about to install. As per this blog post, I filled in the following
  1. Application Tab
    1. Path: C:\Inetpub\solr\solr_start.cmd
    2. Startup Directory: leave the auto-filled value (C:\Inetpub\solr)
  2. Details Tab
    1. Display Name: Apache - Solr 6.1
    2. Startup type: Automatic
  3. Log on Tab
    1. Make sure you specify an account that has administrator-level permissions (I left the "local system account" that was pre-selected)
  4. I/O Tab
    1. I/O Redirection
      1. Output (stdout): I set it to C:\Inetpub\solr\solr-6.1.0-out.txt
      2. Error (stderr): I set it to  C:\Inetpub\solr\solr-6.1.0-error.txt
    2. File rotation
      1. Check Rotate files
      2. Check Rotate while service is running
      3. Restrict rotation to files bigger than: I filled in 4Mb, i.e. 4,194,304 bytes
Hit "Install service" and you're almost set. 

You still need to start the service. So go to Control Panel --> System and Security --> Administrative Tools --> Services and locate the "Apache Solr 6.1" service and start it.

Once started, you can open up the Admin UI in the browser to verify that the instance is indeed running.

Happy searching!

Fun with WFFM Part 2

This is a continuation of my previous post, about WFFM and its quirks (or "bugs", or "features", depending on your point of view).

The Date Selector field

First of all, there are two fields for date picking: a "Date" field, that outputs three dropdowns and a "DatePicker" field that outputs a text field with a jQuery UI DatePicker attached to it. This post will be dealing with the former, though I can see that the latter might have its own issues as well.


Odd HTML output.

The html output has a rather odd structure, where there is 

  • a "span" (actually, an "asp:Label" without an associated control) that takes the content of the "title" field.
  • Three "label" tags, one for "year", one for "month" and one for "day" dropdowns.
  • Respectively, three dropdowns (year, month, day).
There is no additional markup for the labels and dropdowns, They are just output like that, one next to the other, without any grouping between them, they are all sibling HTML elements, with the three labels preceding the three dropdowns. No wrapping div for each label-dropdown pair. That's a lot of fun if you have even the slightest notion of styling the thing.

Undocumented and "Un-interfaced" properties

I mentioned three labels: "year", "month", "day". There is no way to set the values for these labels in a multilingual site, using the form designer. You have to decompile the dll, realize that there are three public properties named "YearLabel", MonthLabel"  and "DayLabel", and then go to Sitecore Content Editor, go to the "Date" field item in your form, and pull your XML-fu to write this snippet in the "Localized Parameters" for the field
<DayLabel>Day</DayLabel><MonthLabel>Month</MonthLabel><YearLabel>Year</YearLabel>
where you replace the node values with what you want written in the respective labels.

Ok, granted, that wasn't too difficult to find, but why hide the ability to edit the labels? I believe the Sitecore team simply forgot to add the required attributes to the properties ("VisualProperty", "DefaultValue", "Localizable"). However, it's really frustrating having to decompile the library just to find out how you're supposed to localize three labels, when  just a small code addition that already exists in other properties in the same field type would have made this a non-issue.

Date formats

There is a specific field in the form designer, called "Date Format". It's a dropdown, that shows a list of date display examples. The source for the droplist is at /sitecore/system/Modules/Web Forms for Marketers/Settings/Meta data/Date Formats. Usually, this is OK. However, for multilingual sites where one language prefers month before day (such as American English) whereas another one prefers day before month (such as pretty much the rest of the world), the fact that the specific parameter is not localized can (and does) pose a serious issue.As far as I know, there's no way around this without actually rolling out your own class, replacing the field type definition class in /sitecore/system/Modules/Web Forms for Marketers/Settings/Field Types/Simple Types/Date.

Multilingual month display

If you choose anything other than "MM" for month, you'll soon find out that the month dropdown always displays the months in English, no matter what language you are viewing the site in. This is due to the following code in the month dropdown population code (InitMonth metod):
            for (int i = 1; i <= 12; i++)
            {
                this.month.Items.Add(new ListItem(string.Format("{0:" + marker + "}", dateTime.AddMonths(i - 1)), i.ToString()));
            }
It should be changed to this:
            for (int i = 1; i <= 12; i++)
            {
                this.month.Items.Add(new ListItem(string.Format(Sitecore.Context.Language.CultureInfo, "{0:" + marker + "}", dateTime.AddMonths(i - 1)), i.ToString()));
            }
Unfortunately, the method is private, so to override it you need to roll out your own copy of the DateSelector class, and not just subclass the original.

Minimum/Maximum/Default date

there are three fields in the form designer to set dates:. Default, Minimum and Maximum. What these do is the following:
  • The Default date sets the date preselected when the page loads
  • The Minimum date sets a validation on the user selected date, as well as the minimum year in the relevant dropdown displayed in the form
  • The Maximum date sets a validation on the user selected date, as well as the maximum year in the relevant dropdown displayed in the form
Apart from an obscure "today" value to the default date, which I'm yet to find a way to pass using the form designer, there is absolutely no way to set these three dates in a rolling, dynamic fashion; you practically hard wire three fixed dates. However, most of the times I've had the pleasure (or torment, depending on your point of view) of creating a form with any kind of date selection, the limits and the default date were supposed to be fluid (e.g. from 2 years ago up until yesterday, with default value yesterday, or from the day after tomorrow until 3 months after today, with the default date being 3 days from now).

Unfortunately, the Sitecore UI for setting these fields (as well as the code supporting them) does not allow for such dynamic settings: the UI raises a calendar, while in the code, an ISO date string is expected. 

This is by far the worst of the quirks for the date selector field. This alone made me roll out my own class - effectively copying and then augmenting the existing class.

My custom class has the following new properties:
        [VisualCategory("Validation"), VisualProperty("Start Date expression:", 2001), DefaultValue("")]
        public string StartDateExp
        {
            get
            {
                return this.classAtributes["StartDateExp"];
            }
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    if (value != "today" && !System.Text.RegularExpressions.Regex.IsMatch(value, @"^[-+]\d+[ymd](\s[-+]\d+[ymd])*$"))
                    {
                        throw new ArgumentException("The value is not a valid expression.", "value");
                    }
                }
                this.classAtributes["StartDateExp"] = value;
            }
        }
        [VisualCategory("Validation"), VisualProperty("End Date expression:", 2001), DefaultValue("")]
        public string EndDateExp
        {
            get
            {
                return this.classAtributes["EndDateExp"];
            }
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    if (value != "today" && !System.Text.RegularExpressions.Regex.IsMatch(value, @"^[-+]\d+[ymd](\s[-+]\d+[ymd])*$"))
                    {
                        throw new ArgumentException("The value is not a valid expression.", "value");
                    }
                }
                this.classAtributes["EndDateExp"] = value;
            }
        }
        [VisualProperty("Selected Date expression:", 101), DefaultValue("")]
        public string SelectedDateExp
        {
            get
            {
                return this.classAtributes["SelectedDateExp"];
            }
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    if (value != "today" && !System.Text.RegularExpressions.Regex.IsMatch(value, @"^[-+]\d+[ymd](\s[-+]\d+[ymd])*$"))
                    {
                        throw new ArgumentException("The value is not a valid expression.", "value");
                    }
                }
                this.classAtributes["SelectedDateExp"] = value;
            }
        }

Replaced the InitYear method with the following:
        private void InitYear(string marker)
        {
            if (!string.IsNullOrEmpty(StartDateExp))
            {
                StartDate = Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, StartDateExp));
            }
            if (!string.IsNullOrEmpty(EndDateExp))
            {
                EndDate = Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, EndDateExp));
            }

            DateTime dateTime = new DateTime(this.StartDateTime.Year - 1, 1, 1);
            this.year.Items.Clear();
            for (int i = this.StartDateTime.Year; i <= this.EndDateTime.Year; i++)
            {
                dateTime = dateTime.AddYears(1);
                this.year.Items.Add(new ListItem(string.Format("{0:" + marker + "}", dateTime), i.ToString()));
            }
            this.year.SelectedValue = this.CurrentDate.Year.ToString();
        }

The fact that this particular method was marked as private in the original Date Selector class, made me copy the entire code from the decompiler and not just subclass the original.

I also replaced the OnInit with the following:
        protected override void OnInit(EventArgs e)
        {
            this.day.CssClass = "scfDateSelectorDay";
            this.month.CssClass = "scfDateSelectorMonth";
            this.year.CssClass = "scfDateSelectorYear";
            this.help.CssClass = "scfDateSelectorUsefulInfo";
            this.generalPanel.CssClass = "scfDateSelectorGeneralPanel";
            this.title.CssClass = "scfDateSelectorLabel";
            this.Controls.AddAt(0, this.generalPanel);
            this.Controls.AddAt(0, this.title);
            this.generalPanel.Controls.AddAt(0, this.help);
            List<string> list = new List<string>(this.DateFormat.Split(new char[]
            {
                '-'
            }));
            list.Reverse();
            if (!string.IsNullOrEmpty(this.SelectedDateExp))
            {
                SelectedDate =Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, SelectedDateExp));
            }
            list.ForEach(new Action<string>(this.InsertDateList));
            list.ForEach(new Action<string>(this.InsertLabelForDateList));
            this.RegisterCommonScript();
            this.month.Attributes["onclick"] = "javascript:return $scw.webform.controls.updateDateSelector(this);";
            this.year.Attributes["onclick"] = "javascript:return $scw.webform.controls.updateDateSelector(this);";
            this.day.Attributes["onclick"] = "javascript:return $scw.webform.controls.updateDateSelector(this);";
        }

And added this:
        private DateTime ApplyTransform(DateTime d, string t)
        {
            var parts = t.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length>1)
            {
                foreach(var part in parts)
                {
                    d = ApplyTransform(d, part);
                }
                return d;
            }
            if (t=="today")
            {
                return DateTime.Now;
            }
            var num = int.Parse(t.Substring(1, t.Length - 2));
            if (t[0] == '-')
            {
                num = -num;
            }
            switch (t[t.Length-1])
            {
                case 'd':
                    return d.AddDays(num);
                case 'm':
                    return d.AddMonths(num);
                case 'y':
                    return d.AddYears(num);
                default:
                    return d;
            }
        }
This is the method that actually does things. First It reads the input from the "Start/End/Default date Expression", which is a string literal of space separated terms of the form (+/-)(number)(marker) e.g.
+2d -3m +1y

The above means "9 months and 2 days from today". The notation is taken from the similar properties of the jQuery UI DatePicker widget.

Then, it splits the input into terms, and applies the terms sequentially on today's date. Full code for the class at the end of this post.

But wait, that's not all! We've dealt with the display, but not the validation of the user input. you see, the dates the user can select using the dropdowns span from January 1st of the minimum year to December 31st of the maximum year. The minimum and maximum dates are more often than not different. There is a validator that handles this, however it also needs to be aware of the newly added "Expression" properties.

I followed the same logic for the validator, simply copied the code from the decompiler into a new class and added the following:
        public string EndDateExp
        {
            get
            {
                return this.classAttributes["enddateexp"];
            }
            set
            {
                this.classAttributes["enddateexp"] = value;
            }
        }

        public string StartDateExp
        {
            get
            {
                return this.classAttributes["startdateexp"];
            }
            set
            {
                this.classAttributes["startdateexp"] = value;
            }
        }
        private string ApplyTransform(string Modifier)
        {
            return Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, Modifier));
        }

        private DateTime ApplyTransform(DateTime date, string Modifier)
        {
            var parts = Modifier.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length > 1)
            {
                foreach (var part in parts)
                {
                    date = ApplyTransform(date, part);
                }
                return date;
            }
            if (Modifier == "today")
            {
                return DateTime.Today;
            }
            var num = int.Parse(Modifier.Substring(1, Modifier.Length - 2));
            if (Modifier[0] == '-')
            {
                num = -num;
            }
            switch (Modifier[Modifier.Length - 1])
            {
                case 'd':
                    return date.AddDays(num);
                case 'm':
                    return date.AddMonths(num);
                case 'y':
                    return date.AddYears(num);
                default:
                    return date;
            }
        }

And changed the OnLoad to the following:
        protected override void OnLoad(EventArgs e)
        {
            if (!string.IsNullOrEmpty(StartDateExp))
            {
                StartDate = ApplyTransform(StartDateExp);
            }
            if (!string.IsNullOrEmpty(EndDateExp))
            {
                EndDate = ApplyTransform(EndDateExp);
            }
            base.ErrorMessage = string.Format(base.ErrorMessage, "{0}", this.StartDateTime.ToString(Constants.LongDateFormat), this.EndDateTime.ToString(Constants.LongDateFormat));
            this.Text = string.Format(this.Text, "{0}", this.StartDateTime.ToString(Constants.LongDateFormat), this.EndDateTime.ToString(Constants.LongDateFormat));
            base.OnLoad(e);
        }
Full code for the validator class at the end of the post.

Sitecore-wise, I opted to create a new field and a new validator, rather than overwrite the existing.

That's it! And it works like a charm!

As promised, code for the DateSelector field:
    [Adapter(typeof(DateAdapter)), ValidationProperty("Value")]
    public class DateSelector : ValidateControl, IHasTitle
    {
        private static readonly string baseCssClassName = "scfDateSelectorBorder";

        protected DropDownList day = new DropDownList();

        protected Panel generalPanel = new Panel();

        protected DropDownList month = new DropDownList();

        protected System.Web.UI.WebControls.Label title = new System.Web.UI.WebControls.Label();

        protected DropDownList year = new DropDownList();

        private DateTime CurrentDate
        {
            get
            {
                DateTime result;
                if (string.IsNullOrEmpty(this.SelectedDate))
                {
                    result = Sitecore.DateUtil.ToServerTime(DateTime.UtcNow);
                }
                else
                {
                    result = Sitecore.DateUtil.IsoDateToDateTime(Sitecore.DateUtil.IsoDateToServerTimeIsoDate(this.SelectedDate));
                }
                return result;
            }
        }

        private DateTime StartDateTime
        {
            get
            {
                return Sitecore.DateUtil.IsoDateToDateTime(this.StartDate);
            }
        }

        private DateTime EndDateTime
        {
            get
            {
                return Sitecore.DateUtil.IsoDateToDateTime(this.EndDate);
            }
        }

        public string Value
        {
            get
            {
                DateTime dateTime = new DateTime(this.StartDateTime.Year + ((this.year.SelectedIndex > -1) ? this.year.SelectedIndex : 0), ((this.month.SelectedIndex > -1) ? this.month.SelectedIndex : 0) + 1, ((this.day.SelectedIndex > -1) ? this.day.SelectedIndex : 0) + 1);
                return Sitecore.DateUtil.ToIsoDate(dateTime.Date);
            }
        }

        [VisualFieldType(typeof(SelectDateField)), VisualProperty("Selected Date:", 100), DefaultValue("today")]
        public string SelectedDate
        {
            get;
            set;
        }
        [VisualProperty("Selected Date expression:", 101), DefaultValue("")]
        public string SelectedDateExp
        {
            get
            {
                return this.classAtributes["SelectedDateExp"];
            }
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    if (value != "today" && !System.Text.RegularExpressions.Regex.IsMatch(value, @"^[-+]\d+[ymd](\s[-+]\d+[ymd])*$"))
                    {
                        throw new ArgumentException("The value is not a valid expression.", "value");
                    }
                }
                this.classAtributes["SelectedDateExp"] = value;
            }
        }

        public override string DefaultValue
        {
            set
            {
                this.SelectedDate = value;
            }
        }

        [VisualFieldType(typeof(DateFormatField)), VisualProperty("Display Format:", 100), DefaultValue("yyyy-MMMM-dd")]
        public string DateFormat
        {
            get
            {
                return this.classAtributes["DateFormat"];
            }
            set
            {
                this.classAtributes["DateFormat"] = value;
            }
        }

        [VisualCategory("Validation"), VisualFieldType(typeof(SelectDateField)), VisualProperty("Start Date:", 2000), DefaultValue("20000101T000000")]
        public string StartDate
        {
            get
            {
                return this.classAtributes["startDate"];
            }
            set
            {
                if (!Sitecore.DateUtil.IsIsoDate(value))
                {
                    throw new ArgumentException("The value is not an iso date.", "value");
                }
                if (!string.IsNullOrEmpty(StartDateExp))
                {
                    this.classAtributes["startDate"] = Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, StartDateExp));
                }
                else
                {
                    this.classAtributes["startDate"] = value;
                }
            }
        }

        [VisualCategory("Validation"), VisualProperty("Start Date expression:", 2001), DefaultValue("")]
        public string StartDateExp
        {
            get
            {
                return this.classAtributes["StartDateExp"];
            }
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    if (value != "today" && !System.Text.RegularExpressions.Regex.IsMatch(value, @"^[-+]\d+[ymd](\s[-+]\d+[ymd])*$"))
                    {
                        throw new ArgumentException("The value is not a valid expression.", "value");
                    }
                }
                this.classAtributes["StartDateExp"] = value;
            }
        }

        [VisualCategory("Validation"), VisualFieldType(typeof(SelectDateField)), VisualProperty("End Date:", 2000)]
        public string EndDate
        {
            get
            {
                return this.classAtributes["endDate"];
            }
            set
            {
                if (!Sitecore.DateUtil.IsIsoDate(value))
                {
                    throw new ArgumentException("The value is not an iso date.", "value");
                }
                if (!string.IsNullOrEmpty(EndDateExp))
                {
                    this.classAtributes["endDate"] = Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, EndDateExp));
                }
                else
                {
                    this.classAtributes["endDate"] = value;
                }
            }
        }
        [VisualCategory("Validation"), VisualProperty("End Date expression:", 2001), DefaultValue("")]
        public string EndDateExp
        {
            get
            {
                return this.classAtributes["EndDateExp"];
            }
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    if (value != "today" && !System.Text.RegularExpressions.Regex.IsMatch(value, @"^[-+]\d+[ymd](\s[-+]\d+[ymd])*$"))
                    {
                        throw new ArgumentException("The value is not a valid expression.", "value");
                    }
                }
                this.classAtributes["EndDateExp"] = value;
            }
        }

        public override string ID
        {
            get
            {
                return base.ID;
            }
            set
            {
                this.title.ID = value + "_text";
                this.day.ID = value + "_day";
                this.month.ID = value + "_month";
                this.year.ID = value + "_year";
                base.ID = value;
            }
        }

        public override ControlResult Result
        {
            get
            {
                return new ControlResult(this.ControlName, this.Value, null);
            }
            set
            {
            }
        }

        [VisualFieldType(typeof(CssClassField)), VisualProperty("CSS Class:", 600), DefaultValue("scfDateSelectorBorder")]
        public new string CssClass
        {
            get
            {
                return base.CssClass;
            }
            set
            {
                base.CssClass = value;
            }
        }

        protected override Control InnerValidatorContainer
        {
            get
            {
                return this.generalPanel;
            }
        }

        protected override Control ValidatorContainer
        {
            get
            {
                return this;
            }
        }

        [Bindable(true), Description("Title")]
        public string Title
        {
            get
            {
                return this.title.Text;
            }
            set
            {
                this.title.Text = value;
            }
        }

        public string DayLabel
        {
            get;
            set;
        }

        public string MonthLabel
        {
            get;
            set;
        }

        public string YearLabel
        {
            get;
            set;
        }

        public DateSelector(HtmlTextWriterTag tag) : base(tag)
        {
            this.EnableViewState = false;
            this.CssClass = DateSelector.baseCssClassName;
            this.classAtributes["DateFormat"] = "yyyy-MMMM-dd";
            this.classAtributes["startDate"] = Sitecore.DateUtil.IsoDateToServerTimeIsoDate("20000101T120000");
            this.classAtributes["endDate"] = Sitecore.DateUtil.ToIsoDate(Sitecore.DateUtil.ToServerTime(DateTime.UtcNow.AddYears(1)));
            this.SelectedDate = Sitecore.DateUtil.ToIsoDate(DateTime.UtcNow);
            this.DayLabel = "Day";
            this.MonthLabel = "Month";
            this.YearLabel = "Year";
            Utils.SetUserCulture();
        }

        public DateSelector() : this(HtmlTextWriterTag.Div)
        {
        }

        public override void RenderControl(HtmlTextWriter writer)
        {
            this.DoRender(writer);
        }

        protected virtual void DoRender(HtmlTextWriter writer)
        {
            base.RenderControl(writer);
        }

        protected override void OnInit(EventArgs e)
        {
            this.day.CssClass = "scfDateSelectorDay";
            this.month.CssClass = "scfDateSelectorMonth";
            this.year.CssClass = "scfDateSelectorYear";
            this.help.CssClass = "scfDateSelectorUsefulInfo";
            this.generalPanel.CssClass = "scfDateSelectorGeneralPanel";
            this.title.CssClass = "scfDateSelectorLabel";
            this.Controls.AddAt(0, this.generalPanel);
            this.Controls.AddAt(0, this.title);
            this.generalPanel.Controls.AddAt(0, this.help);
            List<string> list = new List<string>(this.DateFormat.Split(new char[]
            {
                '-'
            }));
            list.Reverse();
            if (!string.IsNullOrEmpty(this.SelectedDateExp))
            {
                SelectedDate =Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, SelectedDateExp));
            }
            list.ForEach(new Action<string>(this.InsertDateList));
            list.ForEach(new Action<string>(this.InsertLabelForDateList));
            this.RegisterCommonScript();
            this.month.Attributes["onclick"] = "javascript:return $scw.webform.controls.updateDateSelector(this);";
            this.year.Attributes["onclick"] = "javascript:return $scw.webform.controls.updateDateSelector(this);";
            this.day.Attributes["onclick"] = "javascript:return $scw.webform.controls.updateDateSelector(this);";
        }

        protected override void Render(HtmlTextWriter writer)
        {
            int i = this.day.Items.Count;
            while (i <= 31)
            {
                i++;
                this.Page.ClientScript.RegisterForEventValidation(this.day.UniqueID, i.ToString());
            }
            base.Render(writer);
        }

        protected override void OnPreRender(EventArgs e)
        {
            int num = this.CurrentDate.Year;
            if (string.IsNullOrEmpty(this.year.SelectedValue) || !int.TryParse(this.year.SelectedValue, out num))
            {
                num = this.CurrentDate.Year;
            }
            while (this.day.Items.Count > CultureInfo.InvariantCulture.Calendar.GetDaysInMonth(num, this.month.SelectedIndex + 1))
            {
                this.day.Items.RemoveAt(this.day.Items.Count - 1);
            }
            HiddenField hiddenField = new HiddenField
            {
                ID = (this.ID ?? string.Empty) + "_complexvalue",
                Value = this.Value
            };
            if (this.FindControl(hiddenField.ID) == null)
            {
                this.generalPanel.Controls.AddAt(0, hiddenField);
            }
            base.OnPreRender(e);
            this.title.AssociatedControlID = null;
        }

        private void InsertLabelForDateList(string marker)
        {
            char c = marker.ToLower()[0];
            if (c == 'd')
            {
                this.generalPanel.Controls.AddAt(0, new System.Web.UI.WebControls.Label
                {
                    AssociatedControlID = this.day.ID,
                    Text = Translate.Text(this.DayLabel ?? string.Empty),
                    CssClass = "scfDateSelectorShortLabelDay"
                });
                return;
            }
            if (c == 'm')
            {
                this.generalPanel.Controls.AddAt(0, new System.Web.UI.WebControls.Label
                {
                    AssociatedControlID = this.month.ID,
                    Text = Translate.Text(this.MonthLabel ?? string.Empty),
                    CssClass = "scfDateSelectorShortLabelMonth"
                });
                return;
            }
            if (c != 'y')
            {
                return;
            }
            this.generalPanel.Controls.AddAt(0, new System.Web.UI.WebControls.Label
            {
                AssociatedControlID = this.year.ID,
                Text = Translate.Text(this.YearLabel ?? string.Empty),
                CssClass = "scfDateSelectorShortLabelYear"
            });
        }

        private void InsertDateList(string marker)
        {
            char c = marker.ToLower()[0];
            if (c == 'd')
            {
                this.InitDay(marker);
                this.generalPanel.Controls.AddAt(0, this.day);
                return;
            }
            if (c == 'm')
            {
                this.InitMonth(marker);
                this.generalPanel.Controls.AddAt(0, this.month);
                return;
            }
            if (c != 'y')
            {
                return;
            }
            this.InitYear(marker);
            this.generalPanel.Controls.AddAt(0, this.year);
        }

        private void InitDay(string marker)
        {
            this.day.Items.Clear();
            for (int i = 1; i <= 31; i++)
            {
                this.day.Items.Add(new ListItem(i.ToString(), i.ToString()));
            }
            this.day.SelectedIndex = this.CurrentDate.Day - 1;
        }

        private void InitMonth(string marker)
        {
            DateTime dateTime = default(DateTime);
            this.month.Items.Clear();
            for (int i = 1; i <= 12; i++)
            {
                this.month.Items.Add(new ListItem(string.Format(Sitecore.Context.Language.CultureInfo, "{0:" + marker + "}", dateTime.AddMonths(i - 1)), i.ToString()));
            }
            this.month.SelectedIndex = this.CurrentDate.Month - 1;
        }

        private void InitYear(string marker)
        {
            if (!string.IsNullOrEmpty(StartDateExp))
            {
                StartDate = Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, StartDateExp));
            }
            if (!string.IsNullOrEmpty(EndDateExp))
            {
                EndDate = Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, EndDateExp));
            }

            DateTime dateTime = new DateTime(this.StartDateTime.Year - 1, 1, 1);
            this.year.Items.Clear();
            for (int i = this.StartDateTime.Year; i <= this.EndDateTime.Year; i++)
            {
                dateTime = dateTime.AddYears(1);
                this.year.Items.Add(new ListItem(string.Format("{0:" + marker + "}", dateTime), i.ToString()));
            }
            this.year.SelectedValue = this.CurrentDate.Year.ToString();
        }

        protected void RegisterCommonScript()
        {
        }

        private DateTime ApplyTransform(DateTime d, string t)
        {
            var parts = t.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length>1)
            {
                foreach(var part in parts)
                {
                    d = ApplyTransform(d, part);
                }
                return d;
            }
            if (t=="today")
            {
                return DateTime.Now;
            }
            var num = int.Parse(t.Substring(1, t.Length - 2));
            if (t[0] == '-')
            {
                num = -num;
            }
            switch (t[t.Length-1])
            {
                case 'd':
                    return d.AddDays(num);
                case 'm':
                    return d.AddMonths(num);
                case 'y':
                    return d.AddYears(num);
                default:
                    return d;
            }
        }
    }
And also for the validator
    public class DateValidator : FormCustomValidator
    {
        public string EndDate
        {
            get
            {
                return this.classAttributes["enddate"];
            }
            set
            {
                this.classAttributes["enddate"] = value;
            }
        }

        public string StartDate
        {
            get
            {
                return this.classAttributes["startdate"];
            }
            set
            {
                this.classAttributes["startdate"] = value;
            }
        }

        public string EndDateExp
        {
            get
            {
                return this.classAttributes["enddateexp"];
            }
            set
            {
                this.classAttributes["enddateexp"] = value;
            }
        }

        public string StartDateExp
        {
            get
            {
                return this.classAttributes["startdateexp"];
            }
            set
            {
                this.classAttributes["startdateexp"] = value;
            }
        }

        protected DateTime EndDateTime
        {
            get
            {
                return Sitecore.DateUtil.IsoDateToDateTime(this.EndDate).Date;
            }
        }

        protected DateTime StartDateTime
        {
            get
            {
                return Sitecore.DateUtil.IsoDateToDateTime(this.StartDate).Date;
            }
        }

        public DateValidator()
        {
            base.ServerValidate += new System.Web.UI.WebControls.ServerValidateEventHandler(this.OnDateValidate);
            this.StartDate = "20000101T000000";
            this.EndDate = Sitecore.DateUtil.ToIsoDate(DateTime.UtcNow.AddYears(1));
            Utils.SetUserCulture();
        }

        protected override bool EvaluateIsValid()
        {
            bool result;
            try
            {
                if (!string.IsNullOrEmpty(base.ControlToValidate))
                {
                    result = base.EvaluateIsValid();
                }
                else
                {
                    result = true;
                }
            }
            catch (ArgumentOutOfRangeException)
            {
                result = false;
            }
            return result;
        }

        protected override void OnLoad(EventArgs e)
        {
            if (!string.IsNullOrEmpty(StartDateExp))
            {
                StartDate = ApplyTransform(StartDateExp);
            }
            if (!string.IsNullOrEmpty(EndDateExp))
            {
                EndDate = ApplyTransform(EndDateExp);
            }
            base.ErrorMessage = string.Format(base.ErrorMessage, "{0}", this.StartDateTime.ToString(Constants.LongDateFormat), this.EndDateTime.ToString(Constants.LongDateFormat));
            this.Text = string.Format(this.Text, "{0}", this.StartDateTime.ToString(Constants.LongDateFormat), this.EndDateTime.ToString(Constants.LongDateFormat));
            base.OnLoad(e);
        }

        private void OnDateValidate(object source, System.Web.UI.WebControls.ServerValidateEventArgs args)
        {
            DateTime date = Sitecore.DateUtil.IsoDateToDateTime(args.Value).Date;
            if (this.StartDateTime <= date && date <= this.EndDateTime)
            {
                args.IsValid = true;
                return;
            }
            args.IsValid = false;
        }

        private string ApplyTransform(string Modifier)
        {
            return Sitecore.DateUtil.ToIsoDate(ApplyTransform(DateTime.Today, Modifier));
        }

        private DateTime ApplyTransform(DateTime date, string Modifier)
        {
            var parts = Modifier.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length > 1)
            {
                foreach (var part in parts)
                {
                    date = ApplyTransform(date, part);
                }
                return date;
            }
            if (Modifier == "today")
            {
                return DateTime.Today;
            }
            var num = int.Parse(Modifier.Substring(1, Modifier.Length - 2));
            if (Modifier[0] == '-')
            {
                num = -num;
            }
            switch (Modifier[Modifier.Length - 1])
            {
                case 'd':
                    return date.AddDays(num);
                case 'm':
                    return date.AddMonths(num);
                case 'y':
                    return date.AddYears(num);
                default:
                    return date;
            }
        }
    }
Happy Coding!

Fun with WFFM Part 1

WFFM is a great tool. Don't get me wrong. It really is. 

It's just that sometimes it drives me nuts.

The other day, I set out to populate a contact us form, in a multilingual site, on a 8.1 update 2 Sitecore, with its relevant WFFM module installed. I set the form up in English first, added the stock "Send message" action, jotted down a simple email message with the submission data, and tested it. All was well.

Then I started tweaking it. First of all, the form had a dropdown, that did various things on change (via custom javascript of course), so I had to have different display texts and values. No problem there, I just went into Sitecore and did what had to be done in the form editor. But that broke the email message, because now, instead of the nice display text, I got the Sitecore ID of the selected option. Not good.

So I set out and used this amazing solution, ILSpy-ed the "send message" save action and rolled out my own. No biggie, in a matter of an hour I was up and running, with my custom javascript onchange event handlers on the WFFM droplist, and my custom made email sending save action with properly "shoehorned" values. I need to stress out here, that part of the solution is accessing the submitted values (the result set) by name, where name is the Item name of the WFFM fields of the form. Here comes the fun stuff:

Hidden Item Renaming

Then I went on to add the form in the other languages of the site. I had the translations handy, so it was a simple matter of data entry. Right? WRONG! After publishing, much to my horror, my form stopped functioning. I got the dreaded "your data may not have been saved" warning message I so well know and loathe.

So I went into the log, and lo and behold, a null reference exception on a line that accessed a result by name. Wondering what the issue may be, since I had already tested the thing before, I went into Sitecore to check the Item name - perhaps a capitalization issue. Imagine my surprise when I realized that Sitecore had changed the form field item names to the translated values of their titles. So the action could not locate the form field, and it all broke down in tears. Of course I changed the names back, but why would Sitecore rename the items in the first place? I only wanted a translation, not an item rename!

No Multilingual Email Messages

Then I went on to add a html template for the email message sent, with some images, and some fancy text apart from the submission data. I copy pasted the carefully crafted html, that had proper full url's for the images and tried it. All was ok. then I went on to change the language and add its translation, but NO! the Save Actions field, where the entire HTML is stored as html-encoded value of a XML fragment, is in fact a shared field! So no translations for you, mail message! 

Granted, more often than not, that's what you want. But when it comes to emails, which are client-facing things, Sitecore should have provisioned some kind of multilingual support. I found a post saying "just simply unshare the field". THAT'S A HACK IF I EVER SAW ONE! Of course I went on and unshared the field. It's working, but with a HUGE caveat. When you create a new "form" item, the standard values have a specific xml string for the "Save actions" field. You guessed it, when creating a new language version for the form, this field is now empty, thus breaking the sitecore client. My workaround is EVEN HACKIER, as I first switch to raw values, copy the xml from the first language, then create the version in the other language (with the "raw values" on, sitecore doesn't break) then paste the value in the field, save and switch back to normal display. Talk about additional administrative burden!

Eating away the server url from links and image src

Having done that, I went on to check the email sent and OH NO! All url's had their server part stripped! Apparently, that's something the DHTML Editor does. But then, the ProcessMessage pipeline (the one that parses the email, replaces tokens and sends the mail) does not restore them back, if they are not Sitecore links

So all links had to be converted to sitecore links. Fair enough, but with a lot of teeth clenching.

No images can be added via design mode

Fair enough, only Sitecore links. But what about images? The DHTML editor for the email message does not have a "add image" button! Cheap hack: add it via html, then right click on it and open the image properties. You can then go on and replace your dummy (or not so dummy) image with one from the Media Library. The src is now a propersitecore link, so it should be picked up by Sitecore and replaced with a fully qualified url with the servar part in. Only to find out that...

Sitecore links in Image Src attributes are not expanded

Because WFFM wants to throw yet another curveball. Apparently, the AddHostToMediaItem method only expands media item links that are in anchors (i.e. "<a href>"  stuff, not "<img src>" stuff). I had to roll out an additional custom pipeline processor for this (mostly ripped from the stock sitecore one though):

    public class ProcessMessage
    {
        private readonly string shortSrcMediaReplacer;

        public IItemRepository ItemRepository { get; set; }

        public IFieldProvider FieldProvider { get; set; }

        public ProcessMessage() : this(DependenciesManager.WebUtil) { }

        public ProcessMessage(IWebUtil webUtil)
        {
            Sitecore.Diagnostics.Assert.IsNotNull(webUtil, "webUtil");
            this.shortSrcMediaReplacer = string.Join(string.Empty, new string[]
			{
				"src=\"",
				webUtil.GetServerUrl(),
				"/-/"
			});
        }

        public void AddHostToMediaItem(ProcessMessageArgs args)
        {
            args.Mail.Replace("src=\"-/", this.shortSrcMediaReplacer);
            args.Mail.Replace("src=\"/-/", this.shortSrcMediaReplacer);
        }
    }
And then add it in a config patch:
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
     <pipelines>
      <processMessage>
        <processor patch:after="processor[@method='AddHostToMediaItem']" 
                type="MyAssembly.Pipelines.Forms.ProcessMessage, MyAssembly" 
                method="AddHostToMediaItem" />
      </processMessage>
    </pipelines>
  </sitecore>
</configuration>


Finally, I had the form and the action the way I wanted it!

WFFM is fun to work with, but darned if it isn't quirky as can be!

Multi upload from filesystem into Sitecore

There comes a time when you need to perform some heavy duty data entry into Sitecore, and that usually involves a lot of images.

Sitecore does offer a very nifty feature, where you can zip up your media, upload the zip and let Sitecore unpack and create the entire folder and file structure within the Media Library. However, this sometimes falls short, due to web server limitations, e.g. what if the zip file ends up being many hundreds of Mb, with a complex folder and file structure? It will take forever to upload, and also forever to import into Sitecore - if it ever makes it into it.

I've had this issue lately, where I had toy perform a lot of data entry into Sitecore, and I had to import some 300Mb worth of images, in a very complex file structure. Breaking up the zip into smaller zips was not really an option, as even the smaller parts were practically impossible to import.

A bit of inventing googling and some glue code got me the solution to this.

Media item creation

First of all, I got a nifty function to add a file to the Media Library, given I have programmatic access to the file location, from Brian Pedersen, at https://briancaos.wordpress.com/2009/07/09/adding-a-file-to-the-sitecore-media-library-programatically/.

I changed his function just slightly to this:

        public MediaItem AddFile(string filepath, string sitecorePath, string mediaItemName)
        {
            // Create the options
            Sitecore.Resources.Media.MediaCreatorOptions options = new Sitecore.Resources.Media.MediaCreatorOptions();
            // Store the file in the database, not as a file
            options.FileBased = false;
            // Remove file extension from item name
            options.IncludeExtensionInItemName = false;
            // Overwrite any existing file with the same name
            options.OverwriteExisting = true;
            // Do not make a versioned template
            options.Versioned = false;
            // set the path
            options.Destination = sitecorePath + "/" + mediaItemName;
            // Set the database
            options.Database = Sitecore.Configuration.Factory.GetDatabase("master");

            // Now create the file
            Sitecore.Resources.Media.MediaCreator creator = new Sitecore.Resources.Media.MediaCreator();
            MediaItem mediaItem = creator.CreateFromFile(filepath, options);
            return mediaItem;
        }
This is, if you have the sitecore folder path, the new media item name and the file path, the above function will create the media item.

Media item name


To get the media item name, I opted to use the file name without the extension - i.e. what Sitecore does anyway. So I needed to take the filename and remove the last part (the extension). Just splitting on the "." would not cut it, because a name such as "image.date.jpg" is a valid filename, and I would get a "image" instead of "image.date". if I just split on the "." and took the first non empty element.

So I found a function that, given an IEnumerable, returns an IEnumerable with the last N elements removed. Here it is:

        public IEnumerable<T> SkipLastN<T>(IEnumerable<T> source, int n)
        {
            var it = source.GetEnumerator();
            bool hasRemainingItems = false;
            var cache = new Queue<T>(n + 1);

            do
            {
                if (hasRemainingItems = it.MoveNext())
                {
                    cache.Enqueue(it.Current);
                    if (cache.Count > n)
                        yield return cache.Dequeue();
                }
            } while (hasRemainingItems);
        }

Then, to get the Item name, it's just a matter of implementation of the above:
            var itemName=string.Join(".",SkipLastN(file.Name.Split(new[] {'.'}, StringSplitOptions.RemoveEmptyEntries), 1));
Where file is the FileInfo object for the file I want to upload.

File Path


That's just the "FullName" property of the FileInfo object for the file I want to upload.

Sitecore folder path

This was the trickiest one for me. I wanted to maintain the directory structure intact. So what I thought was: 
1. All files to be uploaded must ultimately exist under a single folder. This means my import mechanism would import a folder, its subfolders and files recursively. So I needed a "root" folder path for the source.

2. I might not want to put the uploaded structure directly under the Media Library folder, but under a subfolder in Sitecore. So I needed a "root" folder path for the destination in sitecore.

The end result should be that the contents of the destination root folder in sitecore should reflect the folder and file structure of the source folder one-to-one.

DirectoryInfo to the rescue! I jotted down the following:

        public void ProcessFolder(DirectoryInfo folder, string destination, HttpContext context)
        {
            context.Response.Write(string.Format("Entering {0}...{1}", destination, Environment.NewLine));
            context.Response.Flush();
            foreach(var file in folder.GetFiles())
            {
                processFile(file, destination, context);
            }
            foreach(var subfolder in folder.GetDirectories())
            {
                var sub = subfolder.FullName.Replace(folder.FullName, "").Trim('\\').Trim('/');
                ProcessFolder(subfolder, destination + "/" + sub, context);
            }
        }
The destination string is the Sitecore folder path that corresponds to the folder being processed. As you can see, the function recursively builds the destination folder path, given an initial path and an initial DirectoryInfo object. HttpContext is ther just for reporting reasons (i.e. context.Response.Write).

As for the ProcessFile function:

        public void processFile(FileInfo file, string sitecoreFolder, HttpContext context)
        {
            var itemName=string.Join(".",SkipLastN(file.Name.Split(new[] {'.'}, StringSplitOptions.RemoveEmptyEntries), 1));
            if (string.IsNullOrEmpty(itemName))
            {
                itemName=file.Name.Trim('.');
            }
            var mi = AddFile(file.FullName, sitecoreFolder, itemName);
            context.Response.Write(string.Format("created {0}{1}",mi.Path ,Environment.NewLine));
        }
You can see the IEnumerable popping mthod in action there, as well as the Addfile method.

Putting it all together


Finally, the calling code:
        public void ProcessRequest(HttpContext context)
        {
            context.Response.ContentType = "text/plain";
            var source=context.Request.QueryString["source"].Trim('/');
            if (string.IsNullOrEmpty(source))
            {
                return;
            }
            var directory=context.Server.MapPath(Sitecore.IO.TempFolder.Folder + "/"+source);
            var destination=context.Request.QueryString["destination"].Trim('/');
            if (string.IsNullOrEmpty(destination))
            {
                return;
            }
            var dest = Sitecore.Data.Database.GetDatabase("master").GetItem(Sitecore.ItemIDs.MediaLibraryRoot).Paths.Path + "/" + destination;
            ProcessFolder(new DirectoryInfo(directory), dest, context);
        }
This is just the "ProcessRequest" method of a Generic handler (ashx). I uploaded all the files, in their original directory/file structure, to the temp folder in the Sitecore installation. I then created the "root" folder in Media Library, and ran the above handler, providing source and destination as querystring parameters. So for images uploaded under /temp/MyImages, and destination in /Sitecore/Media Library/SomeSite/Images, and name of handler "ImageImporter.ashx", the url would be
ImageImporter.ashx?source=MyImages&destination=/SomeSite/Images
And that's it! Running the importer, I went through 300Mb worth of images , in about 100 folders and subfolders in less than 20 seconds.

In the link below, you may find the full code for the ImageImporter handler.

Happy coding!

ImageImporter.ashx.cs (4,1KB)

CSS Headaches: IE9 crashes unexpectedly without prompt :/

Have you ever experienced IE9 crashing without prompt while visiting your newly created web site? 
Or even worse, your newly created website is perfectly ok; except one day your client is complaining after incidentally visiting it with IE9?

Your blood becomes cold as ice and wondered what in the heck just happened. What kind of wizard needs to de-compile the entire site in order to find what when wrong...

I have just experienced all this staff, instantly heartlessly and rapidly!

My first response was to start testing the web page by removing whole blocks of staff, such as css or js. I was lucky enough and started removing css files. I quickly discover that a single css file was responsible for this headache. My next move was to remove half css contents (bulk remove), and again half of half content until finally after narrowing my search I end up with 10 lines of css "matter" and therefore I discovered a css property that made the whole headache:

A background property with calc() method:  background-position:calc(100% - 15px) 0;

I just removed it and IE9 smiled again!

So I realized once again: don't mess with IE or your head is cut off. LOL.

For reference:
Calc():  is a very nice and neat method that 
mathematically calculates mixed units such as pixes with percents or ems with percents, etc. This is a real powerful "code ammunition" that makes your life a lot easier. Don't use it unless you are sure you don't care for IE9 users though...

Cheerios,
Nick M.