Telerik, moving to Kendo

I’ve been pretty quiet lately. Spent all my time on our big app, working with the usual MVC, DDD, nHibernate and the like. Much to our content, so far we have managed to keep up with the ever changing landscape of Dutch mental healthcare. For our webpages we were using the Telerik MVC suite, in a previous post I already shared some of our experiences. Telerik is replacing the MVC suite with the new Kendo suite. Which has several very appealing options. Besides that support on the MVC suite is coming to an end. Time to move on.

Replacing all components in one go was not possible. Kendo has the same architecture as the MVC suite and a very similar syntax. But our app is just to large to change all in one go. According to the documentation it’s possible to mix the two suites, even in the same view. To get that actually to work took an effort beyond the faq. In this post I’ll dive into the details.

CSS

Both MVC and Kendo require some style sheets. In the mvc suite you need a StyleSheetRegistrar component to render the css links. Kendo follows the standard way.

Registering the style sheets

Code Snippet
  1. @Html.Telerik().StyleSheetRegistrar().DefaultGroup(group => group.Add(“telerik.common.css”).Add(“telerik.eposoffice2010silver.min.css”).Combined(true).Compress(true))
  2. <linkhref=”@Url.Content(“~/Content/kendo.common.min.css”)rel=”stylesheet”/>
  3. <linkhref=”@Url.Content(“~/Content/kendo.default.min.css”)rel=”stylesheet”/>
  4. <linkhref=”@Url.Content(“~/Content/epos/epos.css”)rel=”stylesheet”type=”text/css”/>

Completely straightforward

jQuery

When it comes to jQuery things get somewhat complicated. The MVC suite requires version <= 1.7, Kendo requires versions >= 1.9. These two are not compatible. Thank goodness the jquery-migrate script library patches the leaks, making it possible for the mvc suite to run using jquey 1.9. The gotcha is that the mvc suite uses a ScriptRegistrar component to register the Telerik scipts. By default this component will also register (the wrong version of) jQuery again. This is prevented by the jQuery(false) method of the registrar.

Code Snippet
  1. <scriptsrc=”@Url.Content(“~/Scripts/jquery-1.11.0.min.js”)type=”text/javascript”></script>
  2. <scriptsrc=”@Url.Content(“~/Scripts/jquery-migrate-1.2.1.min.js”)“></script>
  3. <scriptsrc=”@Url.Content(“~/Scripts/kendo.all.min.js” + “?v=” + version)“></script>
  4. <scriptsrc=”@Url.Content(“~/Scripts/kendo.aspnetmvc.min.js” + “?v=” + version)“></script>
  5. <scriptsrc=”@Url.Content(“~/Scripts/kendo.culture.nl-NL.min.js” + “?v=” + version)“></script>

The MVC suite scripts should be rendered at  the end of the page

Code Snippet
  1. @(Html.Telerik().ScriptRegistrar().jQuery(false).Globalization(true).DefaultGroup(group => group.Combined(true).Compress(true)))

To sum things up. The full master layout

Code Snippet
  1. @using Telerik.Web.Mvc.UI
  2. <!DOCTYPEhtml>
  3. <html>
  4. <head>
  5.     <metacharset=”utf-8″/>
  6.     <title>@ViewBag.Title</title>
  7.     @Html.Telerik().StyleSheetRegistrar().DefaultGroup(group => group.Add(“telerik.common.css”).Add(“telerik.eposoffice2010silver.min.css”).Combined(true).Compress(true))
  8.     <linkhref=”@Url.Content(“~/Content/kendo.common.min.css”)rel=”stylesheet”/>
  9.     <linkhref=”@Url.Content(“~/Content/kendo.default.min.css”)rel=”stylesheet”/>
  10.     <linkhref=”@Url.Content(“~/Content/epos/epos.css”)rel=”stylesheet”type=”text/css”/>
  11.     <scriptsrc=”@Url.Content(“~/Scripts/jquery-1.11.0.min.js”)type=”text/javascript”></script>
  12.     <scriptsrc=”@Url.Content(“~/Scripts/jquery-migrate-1.2.1.min.js”)“></script>
  13.     <scriptsrc=”@Url.Content(“~/Scripts/kendo.all.min.js”)“></script>
  14.     <scriptsrc=”@Url.Content(“~/Scripts/kendo.aspnetmvc.min.js”)“></script>
  15.     <scriptsrc=”@Url.Content(“~/Scripts/kendo.culture.nl-NL.min.js”)“></script>
  16.     @* Scripts required for ajax forms *@
  17.     <scriptsrc=”@Url.Content(“~/Scripts/jquery.unobtrusive-ajax.js”)type=”text/javascript”></script>
  18.     <scriptsrc=”@Url.Content(“~/Scripts/jquery.validate.min.js”)type=”text/javascript”></script>
  19.       <scriptsrc=”@Url.Content(“~/Scripts/jquery.validate.unobtrusive.min.js”)type=”text/javascript”></script>
  20.     @* App specific scripts *@
  21.     <scriptsrc=”@Url.Content(“~/Scripts/Epos.js”)type=”text/javascript”></script>
  22.       <scriptsrc=”@Url.Content(“~/Scripts/tiny_mce/tiny_mce.js”)type=”text/javascript”></script>
  23. </head>
  24. <body>
  25.     <scripttype=”text/javascript”>
  26.         kendo.culture(“nl-NL”);
  27.     </script>
  28.     <div>
  29.         <divid=”maincontent”>
  30.             @RenderBody()
  31.         </div>
  32.         @(Html.Telerik().ScriptRegistrar().jQuery(false).Globalization(true).DefaultGroup(group => group.Combined(true).Compress(true)))
  33.     </div>
  34. </body>
  35. </html>

In case your views use Ajax forms, rendered using Ajax.BeginForm, the ajax scripts are required. Imho these forms are a pita. The handling of events is quite entangling and, even worse, fields edited with a Kendo editor are not included the postdata. For most of our forms we do a straight post using extension methods. Alas, not all views have been adapted yet, so for now the scripts are still required. The good thing is that they still work in the new jquery scenario and don’t seem to bite anything else.

In action

Using this layout both suites can be combined on one page. Here’s a Kendo Colorpicker used within an mvc tabstrip

Untitled

Localization

The way the localization of the components work differs. Kendo is very much script based. For instance the titles of the buttons of the colorpicker above can only be set from script.

To standardize the look and feel of our components we build them using html extension methods, as described here. Also the Telerik Components, which have a lot of options which can be set.  The Kendo suite has an Html extension library, the extension methods in there provide options to configure the component. Alas this list of options is not complete, it is smaller in than the list of configuration options from script. FI it is not possible to set the captions from the extension method for the colorpicker.

All html extension methods do is render some script. It is no great deal to bypass the Kendo extension methods and render the script yourself. The Kendo components all follow the same pattern

Code Snippet
  1. <inputid=”colorpicker”type=”color”/>
  2. <script>
  3.     $(“#colorpicker”).kendoColorPicker({
  4.         buttons: false
  5.     })
  6. </script>

The main difference is that one component renders an input and the other a div.

To get all possibilities of configuration via script you can create your own extension methods which directly render the script

Code Snippet
  1. internalstaticMvcHtmlString KendoColorPickerFor<TModel>(thisHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, string>> expression)
  2. {
  3.     /*
  4.         <input id=”colorpicker” type=”color” />
  5.         <script>
  6.         $(“#colorpicker”).kendoColorPicker({
  7.             messages: {
  8.             apply: “Update”,
  9.             cancel: “Discard”
  10.             }
  11.         })
  12.         </script>
  13.         */
  14.     var id = expression.NameBuilder(htmlHelper.ViewData.Model);
  15.     var value = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model;
  16.     var sb = newStringBuilder();
  17.     sb.AppendFormat(“<input id=’{0}‘ type=’color’ value=’{1}‘/>”, id, value);
  18.     sb.AppendLine(“<script>”);
  19.     sb.AppendFormat(“$(‘#{0}‘).kendoColorPicker({{ messages: {{ apply: ‘OK’, cancel: ‘Annuleren’ }} }})”, id);
  20.     sb.AppendLine(“</script>”);
  21.     returnMvcHtmlString.Create(sb.ToString());
  22. }

The captions are set to OK and annuleren. For now hard coded.

The NameBuilder method is the one from one of my previous stories. It is used to generate an unique id.

Small gotchas

So far mixing Kendo and the classical suite works well. We had two small gotchas so far.

  • Window stacking. In the mvc suite you can stack windows on top of each other. A popup in a popup, In the Kendo suite you can do that as well. This stacking is done using high z-index-es. The classical suite uses a higher range than Kendo. Resulting in a Kendo popup window launched from a classic window appearing behind it’s originator. You cannot stack Kendo and classical windows. Thanks goodness changing from classical window to Kendo window is no big deal.
  • Ajax forms. As mentioned inputs using Kendo components will exclude the edited fields from the ajax postdata. In our app we are switching to posting the data straight from script, using our postdata extension. Far more flexible and no more need for a form.

Winding down

Finding out the way the classical suite and Kendo require jQuery was somewhat of a hurdle. But now all works well and we can convert all further code at our pace. And please our users with a better look and feel. The way the Kendo window automatically adjusts to the size of it’s content is worth the migration on itself.

Posted in Uncategorized | Leave a comment

.Net framework versions and dll hell

With .net version numbers increasing and increasing I recently encountered something which reminded me of dllhell. Which the .net framework promised to end. The nice part is that it already shows up at build time. Not at run time, leading to disappointed customers. Framework versions can be mixed in one solution. Up to the moment one assembly references another assembly built against a higher version. At this moment the VS build process starts to lose track.

The catch is that it’s not always clear you’re mixing framework versions in a solution. Adding a new project will default against the newest framework version. Which is not constant over a solution’s life time. The result can be quite strange. I managed to get away with a circular reference which was not seen because one project was using a version of the other built against an older framework version. And was thus considered a different assembly. Up to the moment I checked the framework version of all projects.

The only good way to solve this descend into hell would be having all projects in the solution on one version. In a big solution with a lot of projects this can be quite tedious. I would love to be able to set the version on the solution level. Or else to have a nice ‘refactoring’ tool to the work for me. R# are you listening ?

 

Posted in Uncategorized | 8 Comments

@model and beyond

In my previous post I had been fiddling with the html helper used in Razor views. Since then our custom html-extensions have been doing great things for our project. To mention some:

  1. Standardizing the look and feel. It is far more consistent and maintainable to set attributes (including a css class) and event handlers in one centralized place.
  2. Simplify the script. Often a part of the logic the script will follow is already known server side. Instead of writing everything out in javascript rendering the intended statements leads to a leaner client. There is an example in my previous post on building post-data.
  3. Decoupling the libraries used. At the moment we are using the Telerik MVC suite. In my previous post I described how our html helpers build standardized Telerik components for our views. In the not to far future we want to switch to the Telerik Kendo suite. Having wrapped up the library dependency in our Html helper will make this switch a lot easier to implement.

What has evolved is the way we work with the model. In MVC the implementation of the controller and the view is clear. When it comes to the implementation of the model there are almost as many different styles of implementation as there are programmers. In general the model can bring any data to the view you can imagine. Not only the source of the data varies, from plain sql to a C# property, also the use of the model’s data varies. It can be a customers name from the database. Or it can be the string representation of some html attribute needed for a fancy picker. Here data and presentation start to get mixed up. Our extensions needed information for the Html-Id. The original Html helper had a custom constructor to get that specific data from the model into the helper. Which required to create our own htmlhelper when starting the view and use that one instead of the standard @html. As seen in the eposHtml in the previous story. It would be cleaner if our extension methods could be satisfied with the default html helper. It would also be cleaner to keep a better separation between ‘real’ data and presentation.

The model is available in every HtmlHelper extension method.

public static PostDataBuilder<TModel> PostData<TModel>(this HtmlHelper<TModel> htmlHelper)

{

    return new PostDataBuilder<TModel>(Id(htmlHelper.ViewData.Model));

}

 

It’s a property of the ViewData.

In our case we needed something to give the control an unique Id. The Id method builds that Id. Previously we passed the Id-base in the constructor, which lead to the custom helper. A far more elegant solution is using a very basic IOC-DI pattern. As implemented By the Id method

private static string Id(object model)

{

    var complex = model as IProvideCompositeId;

    if (complex != null)

        return complex.CompositeId;

    var simple = model as IProvideId;

    return simple == null ? “” :  simple.Id < 0  ? String.Format(“N{0}”, Math.Abs(simple.Id)) : simple.Id.ToString();

}

 

The method queries the model first for the IProvideCompositeId interface, in case the model does not implement that it is queried for the IprovideId interface. Resulting in a string which can be safely used in an HtmlId. (A negative number would lead to a ‘–’ in the string, which is not accepted in an Html Id).

These interfaces are very straightforward

public interface IProvideCompositeId

{

    string CompositeId { get; }

}

 

public interface IProvideId

{

    int Id { get; }

}

 

In case the model is going to be used in a view requiring unique Id’s the model has to implement one of these interfaces.

public class FactuurDefinitie : IProvideCompositeId

{

    public readonly int IdTraject;

    public readonly int UZOVI;

    public readonly bool Verzekerd;

 

    public FactuurDefinitie(int idTraject, int uzovi, bool verzekerd)

    {

        // The usual stuff

    }

 

    public string CompositeId

    {

        get { return String.Format(“{0}{1}{2}”, IdTraject, UZOVI, Verzekerd); }

    }

}

 

Working this way:

  • We can use our custom html extensions in the default html helper
  • Specific data from the model is available inside our extensions
  • The model and the view do not get entangled

The code is no big deal. I know. But the model is something whose horizons are still not in sight.

Posted in Uncategorized | 1 Comment

@Html and beyond

I like the way MVC builds the content of my web-apps. I write plain HTML, and the @Html helper object will build all Html controls needed for in and output of the dynamic data. The nice thing is that the HtmlHelper object is strongly typed, all properties of the view’s model are available as lambda expressions.

<tr>

    <td>

        @Html.LabelFor(m => m.AanmeldingsKlacht)

    </td>

    <td>

        @Html.TextBoxFor(m => m.AanmeldingsKlacht)

    </td>

</tr>

 

Nice clean code.

Things are getting harder when you need more than the standards the HtmlHelper class gives you.

Extending the HtmlHelper

The way to extend the HtmlHelper is by using extension methods. An extension method extends an existing class without inheriting from it like in “classical” oop. Which extension methods are added to an object is ruled by the namespaces used in the code using the extended class.

The methods of the HtmlHelper class should return strings typed as MvcHtmlStrings. A standard string would be Html encoded by the view engine. An MvcHtmlString is literealy rendered into the view. The way to go is to extend the HtmlHelper class with methods which will build plain Html.

As an example of this is Purple, a very simple extension method which does demonstrate the principles and possibilities.

public static MvcHtmlString Purple<TModel, TProperty>(

this HtmlHelper<TModel> htmlHelper,

Expression<Func<TModel, TProperty>> expression)

{

    return new MvcHtmlString(

string.Format(“<span style=’background-color: blueviolet’>{0}</span>”,

                htmlHelper.EditorFor(expression)));

}

 

Note the following points

  • The return type is MvcHtmlString, as discussed above
  • The method is generic. <TModel> is the type of the model of the view where the method is used. <TProperty> is the type of the model’s property.
  • The method is static. The method’s class also has to be static
  • The first parameter this HtmHelper<TModel> hooks the the method into the HtmlHelper object
  • The second parameter expression describes the lambda of the property
  • The htmlHelper object itself, with its rich collection  of methods and properties is available. Here it builds an EditorFor the expression.
  • The result is embedded in a plain Html span with a  “nice”  background

Now when the view uses the class containing this method I can do this:

<tr>

    <td>

        @Html.LabelFor(m => m.HulpvraagInitiatief)

    </td>

    <td>

        @Html.Purple(m => m.HulpvraagInitiatief)

    </td>

</tr>

 

And now the textbox, or whatever other control is built, has a nice purple background. Of course this is not be the way to achieve that effect. But I do hope it clarifies the way the htmlhelper works.

A custom HtmlHelper

We now have a way to extend the HtmlHelper with new methods. These methods can have custom parameters. But what if I needed a custom HtmlHelper with some extra properties which are needed in many instances of many methods ? As a casus I will explore fiddling with the Html Id’s of the controls built.

In the default behavior all EditorFor controls built get an id which is equal to the name of the property. Which is great, when posting back the form TryUpdateModel will bind all posted values to an instance of the model. All of this goes horribly wrong when several instances of a view are rendered on one and the same page. Not just contents gets jumbled up, scripts will go haywire as well.

It is possible to give each control an unique Id by setting it’s HtmlAttributes. Some, like the Telerik suite, even have a method to assign a name. Assigning these Id’s is a hassle and he controller receiving the postdata will also have a lot of work to do to extract all posted values.

Here’s the plan:

  • Build an extended HtmlHelper which takes an Id to uniquify names
  • Give this class EditorFor methods which build controls with an unique name
  • Let the class create the postdata

The class will be generic, typed to the Model

public class MyHtmlbuilder<TModel>

{

    private readonly int _id;

    private readonly HtmlHelper<TModel> _htmlHelper;

 

    public MyHtmlbuilder(int id, HtmlHelper<TModel> htmlHelper)

    {

        _id = id;

        _htmlHelper = htmlHelper;

    }

}

 

In the constructor it receives the Id needed for unique names and the original HtmlHelper object.

To hook into the existing helper and model the builder is again created from an extension method.

 

public static MyHtmlbuilder<TModel> MyHtml<TModel>(this HtmlHelper<TModel> htmlHelper, int id)

{

    return new MyHtmlbuilder<TModel>(id, htmlHelper);

}

 

Having my helper in place I can give it methods to build Html. To create the unique names I need yet another extension method. This one is on the lambda expression to find the property name using reflection.

 

private static string NameBuilder<TModel, TProperty>(this Expression<Func<TModel, TProperty>> expression, int id)

{

    var me = expression.Body as MemberExpression;

    if (me == null)

    {

        throw new ArgumentException(“You must pass a lambda of the form: ‘(object) => object.Property’”);

    }

    return string.Format(“{0}{1}”, me.Member.Name, id);

}

 

The Html building methods are quite simple. Building a dropdownlist for a property:

 

public MvcHtmlString DropDownListFor<TProperty>(Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectListItems)

{

    var id = expression.NameBuilder(_id);

    return _htmlHelper.DropDownListFor(expression,

selectListItems,

new { id }) ;

}

The namebuilder method builds an unique name. The original htmlhelper does the real work. Using my name in the Html-attributes.

 

I can also build third party controls here. Here’s a Telerik datepicker:

 

public MvcHtmlString DatePickerFor(Expression<Func<TModel, DateTime>> expression)

{

    return

        new MvcHtmlString(_htmlHelper.Telerik()

                    .DatePickerFor(expression)

                    .OpenOnFocus(true)

                    .Name(expression.NameBuilder(_id)).ToHtmlString());

}

 

Note that Telerik has it’s own method for setting the name. It’s controls are extremely picky over unique names.

I leave it up to you to add the builder methods you need. Working with this kind of code is somewhat puzzling in the beginning, all the this’s and generics take some time to sink in. But when working with it it soon falls into place. It made me even more delighted over the powers of the C# language.

The helper can do more than building just Html. It’s also great for building script. Posting the form-data as they are will lead to all the funny names leaking back to the controller. It would be great to have the original simple names. To control this you have to post from script.

    $.post(‘/EersteLijnsTraject/Bewaren/?idTraject=@Model.Id’,

        {

            MyPostValue : $(‘#MyPostValue@(Model.Id)‘).val()

   }

        

 

Here a form variable named MyPostValue is posted with its value coming from a control named MyPostValueX. This works but is ugly code which is hard to write and maintain.

Here my custom helper comes to the rescue. It uses a class to build the postdata. The class takes the id in its constructor and uses a stringbuilder to build the script. The Add method adds a model member to the post. Using the pattern described above based on a lambda on the generic TModel type of the data.

public class PostDataBuilder<TModel>

{

    private readonly StringBuilder _sb = new StringBuilder();

    private readonly int _id;

 

    public PostDataBuilder(int id)

    {

        _id = id;

    }

 

    // Add form field to postdata

    public PostDataBuilder<TModel> Add<TProperty>(Expression<Func<TModel, TProperty>> expression)

    {

        var propName = expression.NameBuilder();

        // JQuery  i.e. : BeginDatum : $(‘#BeginDatum1′).val()

        _sb.Append(string.Format(“{0} : $(‘#{0}{1}’).val()”, propName, _id));

        // comma separated

        _sb.Append(‘,’);

        return this;

    }

    // Generate

    public HtmlString Render()

    {

        /*  Resulting in

            BeginDatum : $(‘#BeginDatum1′).val(),

            EindDatum : $(‘#EindDatum1′).val(),               

            DBC : $(‘#DBC1′).val()                

 

            */

        return new HtmlString(_sb.ToString(0, _sb.Length – 1));

    }

}

 

The Render methods builds the desired script snippet.

$.post(‘/EersteLijnsTraject/Bewaren/?idTraject=@Model.Id’,

    {

 

        @(myHtml.PostData().Add(m => m.DatumAanmelding)

                                    .Add(m => m.HulpvraagInitiatief)

                                    .Add(m => m.Aanmelding)

                                    .Add(m => m.AanmeldingsKlacht)

                                    .Add(m => m.DiagnoseSet)

                                    .Add(m => m.Werkzaamheden)

                                    .Add(m => m.Tests)

                                    .Add(m => m.BehandelMethodiek)

                                    .Add(m => m.MedicatieGebruik)

                                    .Render())

        },

        // return function

        function(data) {

 

 

This code is a lot better to read and maintain. Thanks to the lambdas. As an extra bonus posting this way does not require an html-form.

Wrapping up and winding down

Building a custom HtmlHelper is, when you understand how, no big job. Give these snippets of my experiments it should be no big problem to create your own.

It takes these steps to use your own custom helper in a view

  • Make sure the code is included the uses
  • Create a helper in the head of the page
  • Use its methods

Here’s a slightly stripped version of one of our pages using EposHtml, our custom HtmlHelper 

@model ELPTrajectModel

@using Epos4.Website.EposZilos.Helpers

@using Epos4.Website.EposZilos.Models

 

@{

    var eposHtml = Html.EposHtml(Model.Id);

}

 

<script type=”text/javascript”>

function Bewaar@(Model.Id)() {

    $.post(‘/EersteLijnsTraject/Bewaren/?idTraject=@Model.Id,

        {

 

            @(eposHtml.PostData().Add(m => m.DatumAanmelding)

                                     .Add(m => m.HulpvraagInitiatief)

                                     .Add(m => m.Aanmelding)

                                     .Add(m => m.AanmeldingsKlacht)

                                     .Add(m => m.DiagnoseSet)

                                     .Add(m => m.Werkzaamheden)

                                     .Add(m => m.Tests)

                                     .Add(m => m.BehandelMethodiek)

                                     .Add(m => m.MedicatieGebruik)

                                     .Render())

            },

            // return function

            function(data) {

                var grid = $(‘#EersteLijnsTrajecten’).data(‘tGrid’);

                grid.pageTo(grid.currentPage);

            });

    }

</script>

 

 

<table>

    <tr>

        <td>

            @Html.LabelFor(m => m.DatumAanmelding)

        </td>

        <td>

            @eposHtml.DatePickerFor(m => m.DatumAanmelding)

        </td>

    </tr>

…………………………….

    <tr>

        <td>

        </td>

        <td>

            <button type=”button” onclick=”Bewaar@(Model.Id)()”>

                Bewaar</button>

        </td>

    </tr>

</table>

 

You don’t want to see the pages before the custom Helper Glimlach  The way the name of the save method is “uniquified” might give a clue. Anyway, I hope to have inspired you to explore the HtmlHelper as well.

Posted in Uncategorized | 2 Comments

2013

Almost 2013 and my blog still ain’t dead yet. The turning of the year I looks like a good moment for another ride on one of my favorite hobby-horses: looking into the future through the eyes of the past. In my youth pretending to be a cowboy turned into another Texan adventure: exploring space. The images in this post are from a 1959 Golden Press book on the future of space exploration. In 1963 a translation hit the Dutch market which appeared just in time to be swallowed by me.

HNY1

These days there’s nothing futuristic about it, both technologically and social just antique.

It got even better when describing a ‘rocket freight liner’

HNY2

Notice the (mail) bags on the left. The only thing missing is a “Wells Fargo Stage Coach” emblem.

I could go on and on. The lesson I learned once again is that foreseeing the future is not just hard, it’s next to impossible. It still is today. Who would seven years ago could have predicted the current state of (FI) JavaScript or Microsoft ? So next year we can just keep exploring and imagining. And most of all, stay critical on prediction.

And in case you make it into space, don’t forget your soda and sandwich:

HNY3

Happy new year.

Posted in Uncategorized | 1 Comment