Wesley Bakker

Interesting things I encounter doing my job...

Sponsors

News

Wesley Bakker
motion10
Rivium Quadrant 151
2909 LC Capelle aan den IJssel
Region of Rotterdam
The Netherlands
Phone: +31 10 2351035

(feel free to chat with me)

Add to Technorati Favorites

August 2008 - Posts

Outlook does not support mailto links by default

I didn't know that

Outlook does not support the RFC 2822 standard by default. So a standard compliant mailto link that has the addresses separated by comma's simply doesn't work with Outlook. If however the addresses are separated by semicolons it does. Unfortunately Mozilla's Thunderbird doesn't work with semicolons. What to do, what to do?

I would ask my clients that use MS Outlook to navigate to Tools -> Options -> E-mail Options -> Advanced E-mail Options and enable 'Allow comma as address separator' if it was not for MS Outlook to simply ignore this setting.

Cheers,

Wesley

Posted: Aug 29 2008, 03:48 PM by webbes | with no comments
Filed under:
CallbackController

Faster, lighter and better than AJAX

Off course that is in some of the cases and not all of them. AJAX is a very beautifull framework and has some great possibilities. Sometimes however a simple lightweighted callback is all that's needed to perform a simple task. (Read #9 over here). It is, no let me rephrase that, it WAS not the easiest task to implement such a lightweighted callback however. That's where this new CallbackController control comes around the corner. First outline the challenge I was facing.

Complicated project

Right now I'm in the middle of a technical design for a big, interactive Sharepoint 2007 Portal. The problem I encountered though is not very Sharepoint specific. I needed to change control #1 on a specific client event of control #2. Let me explain this a little further. We have a slideshow control that shows images about a region, but as soon as a customer hovers a region on an imagemap, the slideshow must show different images. We have a Google Maps control, and as soon as a client hovers on a specific menu item, the Google maps control needs to show some different pointers. Both the images and pointers however come from a database(in this case a SPList).

MSAjax

I could go for AJAX right away, but my callbacks are very very simple. I do not need to maintain - and thus send back and forth - any viewstate. I do not need to rerender complete controls such as grids, since both mentioned controls are Javascript controls. So all I have to do is set some Javascript properties or call some javascript functions, but i DO need some data from the server so I got to have some way to retrieve this data. Another huge problem is that I can't register a ScriptMethod which lives inside a control. I must be a static method on the page or a webservice. In sharepoint however I can't add any methods to the page. All I can do is program some webparts that contain custom controls and methods.

Callbacks to the rescue

ASP.Net 2.0 comes with callbacks and the ICallbackEventHandler interface and if you look around on the internet you'll find a lot of examples of how to implement the interface on a Page. Nice BUT AGAIN in Sharepoint we work with webparts containing controls and we can't implement interfaces on the page. So that won't work for us. We'll need to implement the interface on a control. With the Callback system we CAN get a GetCallbackEventReference to a control method! So I outlined a few requirements.

  • No javascript to write except for the function that get's executed on callback o'course
  • Calculate a result and return it to the client with a server side eventhandler
  • Easy way to add controls as triggers with different arguments per client event.

The code

As always I have a lot of code here for you guys. The complete source and a sample web using the CallbackController is included in the download. This is just V0.1 and it's written as a proof of concept for my technical design but it does it's job pretty well so far.

using System;
using System.Web.Script.Serialization;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
 
namespace WMB {
    /// <summary>
    /// Summary description for MyCallBackControl
    /// </summary>
    [ParseChildren(typeof(CallbackTrigger), ChildrenAsProperties = true, DefaultProperty = "Triggers")]
    public class CallbackController : Control, ICallbackEventHandler {
        /// <summary>
        /// Initializes a new instance of the <see cref="CallbackController"/> class.
        /// </summary>
        public CallbackController() {
        }
 
 
        /// <summary>
        /// Gets or sets the callback script.
        /// </summary>
        /// <value>The callback script.</value>
        public string CallbackScript { get; set; }
 
        private CallbackTriggerCollection _triggers;
        [DefaultValue((string)null), PersistenceMode(PersistenceMode.InnerDefaultProperty), MergableProperty(false)]
        public CallbackTriggerCollection Triggers {
            get {
                if (_triggers == null) {
                    _triggers = new CallbackTriggerCollection();
                }
 
                return _triggers;
            }
        }
 
        /// <summary>
        /// Returns a JSON representation of the object. Just for an ease of use.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns></returns>
        public static string JsonSerialize(object value) {
            return new JavaScriptSerializer().Serialize(value);
        }
 
        /// <summary>
        /// Raises the <see cref="E:System.Web.UI.Control.PreRender"/> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
        protected override void OnPreRender(EventArgs e) {
            Type callbackControlerType = typeof(CallbackController);
 
            if (!Page.ClientScript.IsClientScriptBlockRegistered(callbackControlerType, this.ClientID)) {
                string clientScript = string.Format("function Trigger_{0}(a,c){{{1}}};\n",
                                               this.ClientID,
                                               Page.ClientScript.GetCallbackEventReference(this, "a", CallbackScript, "c"));
 
                Page.ClientScript.RegisterClientScriptBlock(callbackControlerType,
                                                            this.ClientID,
                                                            clientScript,
                                                            true);
 
            }
 
            if(!Page.ClientScript.IsStartupScriptRegistered(callbackControlerType, this.ClientID)){
                string startupScript = string.Empty;
                foreach(CallbackTrigger trigger in Triggers){
                    startupScript += "\n" + trigger.GetTriggerScript(this);
                }
 
                Page.ClientScript.RegisterStartupScript(callbackControlerType, this.ClientID, startupScript, true);
            }
 
            base.OnPreRender(e);
        }
 
        /// <summary>
        /// Occurs when the <see cref="E:OnCallback"/> method gets called. The <see cref="E:RaiseCallbackEvent"/> does so.
        /// </summary>
        public event EventHandler<CallbackEventArgs> Callback;
 
        /// <summary>
        /// Raises the <see cref="E:Callback"/> event.
        /// </summary>
        /// <param name="e">The <see cref="WMB.CallbackEventArgs"/> instance containing the event data.</param>
        protected virtual void OnCallback(CallbackEventArgs e) {
            EventHandler<CallbackEventArgs> temp = Callback;
            if (temp != null) {
                temp(this, e);
            }
        }
 
        #region ICallbackEventHandler Members
        private string result;
 
        /// <summary>
        /// Returns the results of a callback event that targets a control.
        /// </summary>
        /// <returns>The result of the callback.</returns>
        public string GetCallbackResult() {
            return result;
        }
 
        /// <summary>
        /// Processes a callback event that targets a control.
        /// </summary>
        /// <param name="eventArgument">A string that represents an event argument to pass to the event handler.</param>
        public void RaiseCallbackEvent(string eventArgument) {
            CallbackEventArgs e = new CallbackEventArgs(eventArgument);
            OnCallback(e);
            result = e.Result;
        }
        #endregion
    }
 
    /// <summary>
    /// 
    /// </summary>
    public class CallbackEventArgs : EventArgs {
        /// <summary>
        /// Initializes a new instance of the <see cref="CallbackEventArgs"/> class.
        /// </summary>
        /// <param name="argument">The argument.</param>
        public CallbackEventArgs(string argument) {
            this.Argument = argument;
        }
 
        /// <summary>
        /// Gets or sets the result.
        /// </summary>
        /// <value>The result.</value>
        public string Result { get; set; }
        /// <summary>
        /// Gets the argument.
        /// </summary>
        /// <value>The argument.</value>
        public string Argument { get; private set; }
    }
}

And a page declaration:

   1: <div>
   2:     <asp:Button ID="TimeButton" runat="server" Text="Click me and I'll write the server time. Right click me and I'll write the client time." />
   3:     <WMB:CallbackController
   4:         ID="CallbackController1"
   5:         CallbackScript="WriteTheResultToResultDiv"
   6:         OnCallback="CallbackController1_OnCallback"
   7:         runat="server">
   8:         <WMB:CallbackTrigger
   9:             ControlID="TimeButton"
  10:             ClientEvent="oncontextmenu"
  11:             Argument="new Date()"
  12:             CancleBubble="true" />
  13:         <WMB:CallbackTrigger
  14:             ControlID="TimeButton"
  15:             ClientEvent="onclick"
  16:             CancleBubble="true" />
  17:     </WMB:CallbackController>
  18: </div>

Conclusion

It's now very simple to add callbacks to your page! I do not have the time to explain all details of the code. Simply download the solution and have a look at it. If you do have any questions, please feel free to ask.

Cheers,

Wes

Remember Yield?

The Yield Statement

A lot of the times when developers need some sort of enumeration they tend to create a generic list by default. Even if they realy need to iterate that list only once. Some good examples can be found in solutions offered to this code puzzle at less than dot(which is a great newcomer).

Yield

Instead of creating a list first you can use the yield statement just as well. So here's how I would solve this puzzle:

static void Main() {
    Stopwatch sw = new Stopwatch();
    sw.Start();

    DateTime startDate = DateTime.Now;
    DateTime endDate = startDate.AddYears(10);

    DayOfWeek dayOfWeek = DayOfWeek.Friday;
    int day = 13;

    foreach (DateTime date in DayOfWeekAtMonthDay(startDate, endDate, dayOfWeek, day)) {
        Console.WriteLine(date.ToLongDateString());
    }

    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds);

    Console.ReadLine();
}

private static IEnumerable DayOfWeekAtMonthDay(DateTime startDate, DateTime endDate, DayOfWeek dayOfWeek, int day) {
    DateTime checkDate = startDate;

    int dayDiff = day - checkDate.Day;
    if (dayDiff < 0) {
        checkDate = checkDate.AddMonths(1);
    }

    checkDate = checkDate.AddDays(dayDiff);

    while (checkDate <= endDate) {
        if (checkDate.DayOfWeek == dayOfWeek) {
            yield return checkDate;
        }
        checkDate = checkDate.AddMonths(1);
    }
}

Cheers,

Wes

P.S. Do not use this code because it is really buggy.. it's just to point out the use of yield instead of generic lists.

Posted: Aug 21 2008, 10:58 AM by webbes | with 2 comment(s)
Filed under:
CSV stands for COMMA separated values!

CSV and MS Excel

Did you know that if your write a correct CSV(comma separated values) file while having MS Office Excel installed that file get's an Icon displaying an 'A' followed by a comma. If you double-click the file however, MS Office Excel doesn't recognize that the values in that file are separated by a comma. You need to Choose 'Data -> From Text' from the menu and run an import wizard to really extract the data.

Invalidate the file

If you make the file 'invalid' however by replacing the comma's by semicolons MS Office Excel will recognize your separator and the file get's displayed as data.

Save as

If you choose 'Save As -> Other Formats' from the menu, you can choose 'CSV (Comma delimited)(*.csv) as type. If you open the file with Notepad however,you'll find the semicolon as delimeter.

Cheers,

Wes

ToCSVString Extension method

ToCSVString Extension Method

A friend of mine wanted to create the posibility to export some selected sharepoint list items to a spreadsheet program. So he started thinking and decided to create a new list. The clients selected the items by clicking some filter options. Items are then returned by a CAML query and inserted into that new list. The client could then open the new list and export it with the help of the 'Export to Spreadsheet' Action.

My idea was a little different though. Why copy that data to a list if we can response that list as a CSV file right away if we write a simple extension method that can export a Datatable to a CSV string? So that's what I did.

The code

public static class DataExtensions {
    private static Regex quotedRegex = new Regex(@"\A(?:\A\s+.*|.*\s+\z|.*"".*|.*,.*|.*\n.*)\Z", RegexOptions.IgnoreCase);

    public static string ToCSVString(this string value) {
        if (string.IsNullOrEmpty(value)) {
            return "\"\"";
        }

        string retVal = value;

        if (quotedRegex.IsMatch(value)) {
            retVal = string.Concat("\"", retVal.Replace("\"", "\"\""), "\"");
        }

        return retVal;
    }

    public static string ToCSVString(this DataTable value) {
        StringBuilder stringBuilder = new StringBuilder();

        #region add column names as CSV string to stringbuilder
        int columnsCount = value.Columns.Count;
        int firstColumns = columnsCount - 1;
        int columnCounter = 0;

        for (columnCounter = 0; columnCounter < firstColumns; columnCounter++) {
            stringBuilder.Append(value.Columns[columnCounter].ColumnName.ToCSVString() + ",");
        }

        stringBuilder.AppendLine(value.Columns[columnCounter].ColumnName.ToCSVString());
        #endregion

        #region add each row as CSV string to  stringBuilder
        int firstRows = value.Rows.Count - 1;
        int rowsCounter = 0;
        for (rowsCounter = 0; rowsCounter < firstRows; rowsCounter++) {
            for (columnCounter = 0; columnCounter < firstColumns; columnCounter++) {
                stringBuilder.Append(value.Rows[rowsCounter][columnCounter].ToString().ToCSVString() + ",");
            }

            stringBuilder.AppendLine(value.Rows[rowsCounter][columnCounter].ToString().ToCSVString());
        }

        for (columnCounter = 0; columnCounter < firstColumns; columnCounter++) {
            stringBuilder.Append(value.Rows[rowsCounter][columnCounter].ToString().ToCSVString() + ",");
        }

        stringBuilder.Append(value.Rows[rowsCounter][columnCounter].ToString().ToCSVString());
        #endregion

        return stringBuilder.ToString();
    }
}

It's a raw and first implementation and needs some refactoring but it does it's job perfectly. All he had to do was to write that string to the ResponseStream with the correct FileDisposition and MimeType headers and that's it.

Cheers,

Wes

Posted: Aug 20 2008, 03:36 PM by webbes | with no comments
Filed under: , ,
More Posts