24 Dec 2010

ASP.NET MVC 2 Freelance Contractor in Leeds, Yorkshire

I'm a Freelance C# / ASP.NET web application developer based in Leeds, Yorkshire, UK. I've years of experience and have just finished a funky AJAX-heavy MVC contract with oodles of Entity Framework fun, and my usual attention to User Experience. Do you want to hire me? In January I'll be free to do another contract so please get in touch if you think I could help!

17 Dec 2010

Lenovo T410 2-finger scroll edge problem

I love my new Lenovo T410, but the 2-finger scrolling didn't seem to be working right. The area of the touchpad in which the scroll would be detected seemed too narrow. Turns out that "gesture filtering" was enabled and needed to be turned off.

Go to mouse properties / ultranav / touchpad / settings / Smart Check Settings / Gesture Filtering

15 Dec 2010

Castle ActiveRecord / NHibernate Update Event Interceptor to Log Dirty Property Changes

Man I love the Castle Project documentation. Or at least, I would if I was a robot with a brain the size of a planet and an unlimited amount of time on my hands. Sadly I'm a human and find the whole thing impenetrable.

All I wanted to do was to intercept the NHibernate entity update event so that I could log the changes made. With kudos to this dude and this dude, here's how I did it:

1. In my project create a new class:

using Castle;
using Castle.ActiveRecord;
using Castle.ActiveRecord.Attributes;
using NHibernate;
using NHibernate.Event;

Note it uses a struct called EntityPropertyUpdate and a collection called RuntimeManager.EntityPropertyUpdates to keep a log of the changes. You'll have to roll your own if you want to do what I'm doing here, otherwise just modify the code as you see fit.

public class AuditUpdateListener : IPostUpdateEventListener
private const string _noValueString = "EMPTY";

private static string getStringValueFromStateArray(object[] stateArray, int position)
var value = stateArray[position];

return value == null || value.ToString() == string.Empty
? _noValueString
: value.ToString();

public void OnPostUpdate(PostUpdateEvent evt)
if (!(evt.Entity is Log_G))
var dirtyFieldIndexes = evt.Persister.FindDirty(evt.State, evt.OldState, evt.Entity, evt.Session);
foreach (var dirtyFieldIndex in dirtyFieldIndexes)
var oldValue = getStringValueFromStateArray(evt.OldState, dirtyFieldIndex);
var newValue = getStringValueFromStateArray(evt.State, dirtyFieldIndex);

if (oldValue != newValue)
RuntimeManager.EntityPropertyUpdates.Add(new RuntimeManager.EntityPropertyUpdate { EntityName = evt.Entity.GetType().Name, PropertyName = evt.Persister.PropertyNames[dirtyFieldIndex], PropertyFrom = oldValue, PropertyTo = newValue });

The other key thing is the ActiveRecordStarter.Initialize() call, which had to be changed to include the assembly containing the AuditUpdateListener class, which happened to be the main executing assembly. I was doing my AR Initialisation in global.asax so it now looks like this:

protected void Application_Start()
// ActiveRecord Setup
IConfigurationSource source = ActiveRecordSectionHandler.Instance;
Assembly asm = Assembly.Load("MyApp.DataLayer");
ActiveRecordStarter.Initialize(new Assembly[] {asm, Assembly.GetExecutingAssembly()}, source, new Type[] { });

That's all!

Incidentally though, if you want to use an ambient AR SessionScope like I do, add this to global.asax:

protected void Application_BeginRequest(Object sender, EventArgs e)
// Create a SessionScope for ActiveRecord
HttpContext.Current.Items.Add("ar.sessionscope", new SessionScope(FlushAction.Never));

protected void Application_EndRequest(object sender, EventArgs e)
// Close the SessionScope for ActiveRecord
SessionScope scope = HttpContext.Current.Items["ar.sessionscope"] as SessionScope;
if (scope != null)
catch (Exception ex)
HttpContext.Current.Trace.Warn("Error", "EndRequest: " + ex.Message, ex);

6 Dec 2010

MVC File Upload - Using Uploadify with ASP.NET MVC 2 / 3

NOTE 23 Oct 2012

I no longer recommend the use of Uploadify with MVC. I've found benefit using javascript-only solutions such as DropZone : http://www.dropzonejs.com/

or Valum's File Uploader : https://github.com/Valums-File-Uploader/file-uploader

Uploadify is ace for performing file upload and image upload, but ASP.NET MVC docs were lacking, so I hope this helps someone. Critical points addressed here include:
  • Uploadify flash upload button is ugly so we use CSS to make it invisible and fake an HTML button under it.

  • The Uploadify flash upload doesn't pass the ASP.NET session and authentication cookies naturally, which we want for recognition and authorization, so we handle that.


Put all Uploadify files in a project folder called ClientScript/uploadify


Put this code on the regular View page that performs the upload.

 <script type="text/javascript" src="<%= Url.Content("~/ClientScript/jquery-1.4.2.min.js")  %>"></script>
 <script type="text/javascript" src="<%= Url.Content("~/ClientScript/uploadify/swfobject.js") %>"></script>
 <script type="text/javascript" src="<%= Url.Content("~/ClientScript/uploadify/jquery.uploadify.v2.1.4.min.js")  %>"></script>
 <script type="text/javascript">

    $(function () {

        // Uploadify File Upload System
        // SessionSync data is sent in scriptData for security reasons, see UploadifySessionSync() in global.asax

        var UploadifyAuthCookie = '<% = Request.Cookies[FormsAuthentication.FormsCookieName] == null ? string.Empty : Request.Cookies[FormsAuthentication.FormsCookieName].Value %>'; 
        var UploadifySessionId = '<%= Session.SessionID %>';


            'hideButton'   : true,       // We use a trick below to overlay a fake html upload button with this hidden flash button                         
            'wmode'      : 'transparent', 
            'uploader': '<%= Url.Content("~/ClientScript/uploadify/uploadify.swf") %>',
            'cancelImg': '<%= Url.Content("~/ClientScript/uploadify/cancel.png") %>',
            'buttonText': 'Upload File',
            'script': '<%= Url.Action("FileUpload", "Media") %>',
            'multi': true,
            'auto': true,
            'scriptData' : { RequireUploadifySessionSync: true, SecurityToken: UploadifyAuthCookie, SessionId: UploadifySessionId },
            'onComplete' : function (event, ID, fileObj, response, data) 
                                response = $.parseJSON(response);
                                if (response.Status == 'OK')
         // Put your own handler code here instead of the following...
                                    alert('File Uploaded OK!');



Put this HTML on the same View as the script above.

    <div style="position: relative;">
        <input type="button" id="btnUpload" value="Upload File" />

        <div style="position: absolute; top: 4px; left: 3px;">
            <input id="fuFileUploader" name="file_upload" type="file" />


Add the following bits.

 protected void Application_BeginRequest(Object sender, EventArgs e) 
  if (HttpContext.Current.Request["RequireUploadifySessionSync"] != null)

 /// <summary>
 /// Uploadify uses a Flash object to upload files. This method retrieves and hydrates Auth and Session objects when the Uploadify Flash is calling.
 /// </summary>
 /// <remarks>
 ///     Kudos: http://geekswithblogs.net/apopovsky/archive/2009/05/06/working-around-flash-cookie-bug-in-asp.net-mvc.aspx
 ///     More kudos: http://stackoverflow.com/questions/1729179/uploadify-session-and-authentication-with-asp-net-mvc
 /// </remarks>
 protected void UploadifySessionSync()
   string session_param_name = "SessionId";
   string session_cookie_name = "ASP.NET_SessionId";

   if (HttpContext.Current.Request[session_param_name] != null)
    UploadifyUpdateCookie(session_cookie_name, HttpContext.Current.Request.Form[session_param_name]);
  catch {}

   string auth_param_name = "SecurityToken";
   string auth_cookie_name = FormsAuthentication.FormsCookieName;

   if (HttpContext.Current.Request[auth_param_name] != null)
    UploadifyUpdateCookie(auth_cookie_name, HttpContext.Current.Request.Form[auth_param_name]);
  catch {}

 private void UploadifyUpdateCookie(string cookie_name, string cookie_value)
  HttpCookie cookie = HttpContext.Current.Request.Cookies.Get(cookie_name);
  if (cookie == null)
   cookie = new HttpCookie(cookie_name);
  cookie.Value = cookie_value;

    public class MediaAssetUploadModel
        public HttpPostedFileBase fileData { get; set; }
        public string SecurityToken { get; set; }
        public string Filename { get; set; }

 public ActionResult FileUpload(MediaAssetUploadModel uploadedFileMeta)
  string fullFilePath = SaveUploadedFile(uploadedFileMeta);
  //TODO: Error handling
  return Json(new { Status = "OK" });

 //TODO: move this into a manager/repository class
 private string SaveUploadedFile(MediaAssetUploadModel uploadedFileMeta)
  string fileName = Guid.NewGuid() + System.IO.Path.GetExtension(uploadedFileMeta.Filename);
  string fullSavePath = Path.Combine(ConfigurationManager.AppSettings["MediaAssetFolder"], fileName);

  return fullSavePath;


Make following changes to upload files to c:\temp , with a max file size of 100Mb.
Change these to suit yourself - c:\temp will get automatically cleared so you have been warned.
If you create your own upload folder, remember you will need to give write perms to the ASP.NET process.
  <add key="MediaAssetFolder" value="c:\temp"/>
  <httpRuntime maxRequestLength="100000" /> 

26 Nov 2010

Simple Jquery Lightbox

Wrote some code that I didn't end up using, but it's a shame to bin it :)

Simple lightbox using Jquery UI dialog widget:

function LightBox(imgSrc, imgTitle)
var img = $('<img />');
img.attr('src', imgSrc);
var lightBox = $('<div></div>');
lightBox.dialog({ autoOpen: false, height: img.height() + 50, width: img.width() + 50, modal: true, title : imgTitle });

12 Nov 2010

Duffs Lodge

We stayed at Duff's Lodge, Beaufort Estate nr. Beauly, Inverness-shire. Nice.

Would just like to say that the Priory Hotel in Beauly makes the worst sunday roast in the known world.

On the plus side, food and drink at The Cawdor Tavern is bloody lovely.

Apart from that, the road to Skye from Inverness is gobsmacking, and Glen Affric is beautiful.

That is all! Back to the C# shenanigans.

Beef Stew

Man make Stew! Stew good. Stew good for 3 people easy.

400g Beef cubes.
6 Mushrooms
4 Potatoes
1 Leek
1 Onion
3 Large carrots
600ml Beef Stock

Simmer tatties, leek and carrots for 15 mins.
Meanwhile, fry onion, beef and mushrooms for 10 mins.
Add stock to beef pan, then add the veggies.
Simmer the lot for 45 mins.

Nyom! Simmer longer and slower for added nyom.

28 Oct 2010

SharePoint 2010 Static Web Part to read Page Properties

• Create a new page property by creating a new Site Column
• Edit the Page content type and add the new Site column
• Now all pages will have the new property to edit
• Create your web part to read the Page Property, like

if (SPContext.Current.Fields.ContainsField("Your Page Property Name") && SPContext.Current.Item["Your Page Property Name"] != null)
myString = SPContext.Current.Item["Your Page Property Name"].ToString();
catch { }

• Install and Deploy the web part on the server
• Find the public key token of your part by looking it up under c:\windows\assemblies
• Register the web part as static in your masterpage or page layout:

<%@ Register Tagprefix="Applicable" Namespace="ApplicableWebPartLib.TwitterBlock" Assembly="ApplicableWebPartLib, version=,Culture=neutral,PublicKeyToken=2ee629bf628499ee" %>

• Use the web part on your page layout or master page:

<Applicable:TwitterBlock runat="server"></Applicable:TwitterBlock>

27 Oct 2010

Documenting C# ASP,NET Code Projects and Solutions

We all know we should be using ///<summary> and ///<remark> tags in our code to document it properly, right?

The cool thing about that is that you can then generate an XML file from Visual Studio with all your code comments in it.
To do that, right-click on your Project in Solution Explorer and choose the "Build" tab. Check the box marked "XML documentation file" and voila - next time you build, the XML file will be generated too.

Then what? Then, my friend, you can distribute the xml file with your dll, and other developers will be able to see your comments on your class methods and properties.

Also, you can generate a Help file (CHM)! To do that, download and install Sandcastle and the SandCastle Help File Builder (SHFB).

Then in SHFB, you can just drag in your DLL and the XML file will be automatically included.
Build your Help File and a CHM will be generated!
Open it from a local drive (CHMs don't work on a remote drive) and enjoy!

20 Oct 2010

Getting Schema Information from SQL Server

As an aide-memoire to myself and anyone else who cares, here is a (growing, unfinished) list of the various ways you can get schema data and metadata out of SQL Server.




2. sys_objects

3. sp_tables, sp_columns

18 Oct 2010

Sharepoint 2010 Page Properties - Push Changes To Sites and Lists

I'm a bit confused, and it's no wonder. I'm trying to add a custom metadata field to the Page Properties of all the Article Pages on my Publishing Site, and all its subsites. However, when I made the change by creating a Site Column in Sharepoint 2010 Designer, and adding it to the Article Page Content Type column list, the column did not turn up on my subsite pages.

Then I noticed that when editing Content Type columns, there's a button in the ribbon marked "Push Changes to Sites and Lists". I can't find any documentation as to how this button works, or even if it is a button at all, as clicking it seems to make it glow yellow, as if it's a status indicator. Even spookier, at this moment a Google search for "Push Changes to Sites and Lists" comes up with ZERO results!

So, let's open this up for discussion! What is this button for? I *think* it has to be enabled (i.e. glowing yellow) when I make a column change, but BEFORE I press Save, in order for any lists or subsites using the Content Type to register the change.

1 Oct 2010

Delete a User from the ASP.NET membership database

Here's a sweet stored procedure to delete an ASP.NET user by only passing through a UserName, and not the 4 obscure arguments that the aspnet_Users_DeleteUser sproc requires.

CREATE PROCEDURE [dbo].[DeleteUser]
@UserName nvarchar(50)

DECLARE @Error int;
DECLARE @UserGUID uniqueidentifier;


SELECT @UserGUID = UserID FROM aspnet_Users WHERE UserName = @UserName;
DELETE FROM aspnet_Profile WHERE UserID = @UserGUID;
SET @Error = @@ERROR; IF @Error <> 0 GOTO ErrorHandling;
DELETE FROM aspnet_UsersInRoles WHERE UserID = @UserGUID;
SET @Error = @@ERROR; IF @Error <> 0 GOTO ErrorHandling;
DELETE FROM dbo.aspnet_PersonalizationPerUser WHERE UserID = @UserGUID;
SET @Error = @@ERROR; IF @Error <> 0 GOTO ErrorHandling;
DELETE FROM aspnet_Membership WHERE UserID = @UserGUID;
SET @Error = @@ERROR; IF @Error <> 0 GOTO ErrorHandling;
DELETE FROM aspnet_users WHERE UserID = @UserGUID;
SET @Error = @@ERROR; IF @Error <> 0 GOTO ErrorHandling;

GOTO Final


GOTO EndOfProc



SELECT @Error as Result

28 Sept 2010

Sharepoint 2010 Global Navigation Hide First Item (Collection Tab)

I'm branding up a Sharepoint 2010 site at the moment and one thing that stuck in my craw was the Global Navigation. It shows the subsites and pages fine and dandy, but it also sticks the root site collection as the first item in the menu.

Now, you can probably remove it programatically with a solution like this, but I just wanted to do it with some style tweaks.

So just stick this in your custom css file:

#s4-topheader2 a.static.menu-item[accesskey="1"] { display: none !important; }

That works for most browsers but not IE6 (spit) cos of it's CSS2 failitude. For that you can use some sweet jQuery in a jiffy:

<script type="text/javascript">
$(function () {
$('#s4-topheader2 a.static.menu-item:first').hide();

27 Sept 2010

SharePoint 2010 - Deploy a Custom WebPart

Build a release version of your web part to create a .WSP file

Copy the WSP file to the target server

On the target machine, ensure you have correct SPShellAdmin permissions for the Sharepoint Management PowerShell:
Add-SPShellAdmin –UserName DOMAIN\YOUR_USER

As per this excellent article:

- Add-SPSolution c:\your.WSP

- Install-SPSolution –Identity your.WSP –WebApplication http://yourSPInstance -GACDeployment

- Go to the Site Settings page, choose the Web Parts gallery and Add a New Web Part. Your custom web part should be there and you can Edit it to set the title, description and group.

To do an update later if you've amended your web part code:

Update-SPSolution -Identity your.WSP -LiteralPath c:\your.wsp -GACDeployment

24 Sept 2010

Develop a custom editable Visual Web Part (WebPart) for SharePoint 2010

I wanted to create a web part for Sharepoint 2010 that would let an editor add a block of free-form html to a page, but wrapped up in a nicely formatted HTML container of my choosing. I also wanted the web part to include some custom properties to allow the user to select some permutations for the HTML container (colour, position etc.). The key thing was that when the editor is amending the content, I wanted to be able to use the standard ribbon controls instead of having to hook in a 3rd party rich text control like Telerik RadEditor.

So, I assumed I could just take the build-in Content Editor Web Part (CEWP) and extend it. No dice - cos it's sealed!

So I ended up whipping out Reflector and digging into the CEWP code, ripping the guts out of it to hack a new webpart. The following Web Part does what I set out to do, and maybe it will be a good base for you too.

Step 1: Create a new Visual Web Part called DCContentBlock.

Step 2: In the ASCX template, paste this:
<div class="DCContentBlock">
 <div class="DCContent">
  <asp:Panel ID="plhContentEdit" runat="server" />
  <asp:Panel ID="plhContentDisplay" runat="server" />
  <asp:Panel ID="plhNoContent" runat="server" />

Step 3: In the UserControl class in the ASCX.CS file, paste this:
  public Panel BodyContentEdit { get { return plhContentEdit; } }
  public Panel BodyContentDisplay { get { return plhContentDisplay; } }
  public Panel BodyNoContent { get { return plhNoContent; } }

Step 4: In the web part CS file, here is the main code to make this sucker work:
 public class DCContentBlock : Microsoft.SharePoint.WebPartPages.WebPart
  // Visual Studio might automatically update this path when you change the Visual Web Part project item.
  private const string _ascxPath = @"~/_CONTROLTEMPLATES/YOUR_PROJECT_NAME_HERE/DCContentBlock/DCContentBlockUserControl.ascx";

  private string _content;
  private DCContentBlockUserControl control;

  private HtmlGenericControl editableRegion = new HtmlGenericControl();
  private HtmlGenericControl emptyPanel = new HtmlGenericControl();

  private bool IsInEditMode
    SPWebPartManager currentWebPartManager = (SPWebPartManager)WebPartManager.GetCurrentWebPartManager(this.Page);
    return (((currentWebPartManager != null) && !base.IsStandalone) && currentWebPartManager.GetDisplayMode().AllowPageDesign);

  protected override void OnInit(EventArgs e)
   if (this.IsInEditMode)
    SPRibbon current = SPRibbon.GetCurrent(this.Page);
    if (current != null)
     if (!(this.Page is WikiEditPage))

  protected override void OnLoad(EventArgs e)

   // Prevent default display of webpart chrome in standard view mode
   this.ChromeType = PartChromeType.None;

   control = (DCContentBlockUserControl)Page.LoadControl(_ascxPath);
   control.BodyContentDisplay.Controls.Add(new LiteralControl(this.Content));
   string strUpdatedContent = this.Page.Request.Form[this.ClientID + "content"];
   if ((strUpdatedContent != null) && (this.Content != strUpdatedContent))
    this.Content = strUpdatedContent;
     SPWebPartManager currentWebPartManager = (SPWebPartManager)WebPartManager.GetCurrentWebPartManager(this.Page);
     Guid storageKey = currentWebPartManager.GetStorageKey(this);
    catch (Exception exception)
     Label child = new Label();
     child.Text = exception.Message;
   if (this.IsInEditMode)
    this.Page.ClientScript.RegisterHiddenField(this.ClientID + "content", this.Content);

    control.BodyContentDisplay.Visible = false;

    this.emptyPanel.TagName = "DIV";
    this.emptyPanel.Style.Add(HtmlTextWriterStyle.Cursor, "hand");
    this.emptyPanel.Controls.Add(new LiteralControl("Click here to Edit"));
    this.emptyPanel.Style.Add(HtmlTextWriterStyle.TextAlign, "center");
    base.Attributes["RteRedirect"] = this.editableRegion.ClientID;
    ScriptLink.RegisterScriptAfterUI(this.Page, "SP.UI.Rte.js", false);
    ScriptLink.RegisterScriptAfterUI(this.Page, "SP.js", false);
    ScriptLink.RegisterScriptAfterUI(this.Page, "SP.Runtime.js", false);
    this.editableRegion.TagName = "DIV";
    this.editableRegion.InnerHtml = this.Content;
    this.editableRegion.Attributes["class"] = "ms-rtestate-write ms-rtestate-field";
    this.editableRegion.Attributes["contentEditable"] = "true";
    this.editableRegion.Attributes["InputFieldId"] = this.ClientID + "content";
    this.editableRegion.Attributes["EmptyPanelId"] = this.emptyPanel.ClientID;
    this.editableRegion.Attributes["ContentEditor"] = "True";
    this.editableRegion.Attributes["AllowScripts"] = "True";
    this.editableRegion.Attributes["AllowWebParts"] = "False";
    string script = "RTE.RichTextEditor.transferContentsToInputField('" + SPHttpUtility.EcmaScriptStringLiteralEncode(this.editableRegion.ClientID) + "');";
    this.Page.ClientScript.RegisterOnSubmitStatement(base.GetType(), "transfer" + this.editableRegion.ClientID, script);

  // Properties
  public string Content
    return this._content;
    _content = value;

STEP 5: Your CS file will also need the following using statements at the top:
using System;
using System.ComponentModel;
using System;
using System.Globalization;
using System.Collections.Generic;
using System.ComponentModel;
using System.Xml.Serialization;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebPartPages;
using Microsoft.SharePoint.WebControls;
using Microsoft.Web.CommandUI;

STEP 6: The use of Microsoft.Web.CommandUI means we need to add a project reference to the appropriate dll.
  • Right click on the project and Add Reference.
  • Click Browse and find: C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\ISAPI\Microsoft.Web.CommandUI.dll

That's all! Build it, edit the page, insert a web part and the part will be in the custom folder. Edit the project's .webpart file and the Elements.xml file if you want to change the name, description and group that the webpart belongs to.

After this, you can extend and style the control to your heart's content. Some ideas:

1. Use CSS to style the DCContentBlock and DCContent classes
2. Add your own HTML to the webpart's ASCX template
3. Add editor properties to allow the user to select from some preset styling options.

I went to the additional trouble of adding editor properties by following this excellent article by Sahil Malik. When the user selects from my custom editor property drop-down, I store the preference value. I then refer to this value to append an appropriate css class to the DCContentBlock div in the WebPart's OnPreRender. Let me know if you'd like me to do a separate post on how to do that.

12 Sept 2010

Change / Remove Audi A4 Wheel

Although a CODING GOD, I am admittedly a mechanical weenie. So I just spent about an hour changing a wheel on my Audi cos it had a puncture on my one of my new front tyres, grrrrr. Here's what I learnt.

1. All the kit is located in under the boot/trunk floor, on either side of the spare wheel.
2. Take careful note of the layout of the stowed jack and tools, because they are impossible to put back in any other configuration - it's like a bloody chinese puzzle.
3. The Wheel is held on by 5 nuts (may be more or less on your alloys!) around a central hub. YOU ONLY HAVE TO REMOVE THE NUTS AROUND THE HUB - the rusty hub nut in the middle is not for you to touch! ;)
4. The nuts may be covered by cosmetic hexagonal covers. In the toolkit you'll find a plastic pair of tweezers which are designed to remove these covers.
5. The plastic centre cover of the wheel comes out easily by prising it out carefully with a screwdriver. Allegedly you should instead use the twisty wire thing in the toolkit to do this, but my centre cover did not have an appropriate hole to use it with!? No matter.
6. Loosen the nuts using the breaker bar in the toolkit WHEN THE WHEEL IS STILL ON THE GROUND. Jumping on your breaker bar while the car is up on the jack may end in tears.
7. After the nuts have been removed, it seemed like the wheel was still stuck on (hence me worrying about whether the centre nut had to be removed). But no, the wheel was just seized onto the rusty hub. I couldn't break the seal with my own force - instead i let the jack down and allowed the weight of the car to break the seal instead, then jacked it up again. The wheel came right off then.

10 Sept 2010

CampTune vs WinClone for BootCamp Partition Resize

Hello! I have a Mac Mini with Snow Leopard and Windows 7 running from the same drive via BootCamp. I made a 32Gb windows partition originally, planning to keep all my big windows files on a separate drive. Sadly, 32Gb is still not enough for the windows C drive when you factor in the pagefile, windows update / install files etc.

Therefore, I had to increase the size of the Windows partition without losing either the Mac or the Windows installation. 2 solutions were considered, WinClone and CampTune.

To cut to the chase, WinClone didn't work, as it's been abandoned for a while around Leopard time. I tried it, but the initial backup failed with errors, which gave me the willies. So I shelled out 13 quid for CampTune, which burns a boot CD from which you perform the partition resizing.

It worked like a champ, taking about 20 minutes to do it's thang, leaving both Mac OSX and Windows running just like before, only now with 200Gb for my Win C drive :)

29 Aug 2010

The Girl Who Wrote A Shopping List

It was raining as Mikael Blomkvist took the first stop off the tunnelbana at Likstoerp and visited the late-night AftonMarket. He bought pickled herring, appelsaúce, two pakkenpanes of wholegrain bread, a strudel, some milk, Sverigen cheese, a packet of raisins, three pepparsausage Billy Pan pizzas, a sponge, some shoelaces, a coathanger, some poptarts, a tube of Sensodyne Pro-Enamel toothpaste, a new washbag and a carton of Thor-brand extra strong condoms.

He paid by American Express, and then walked into the Apple Store opposite where he treated himself to a new Apple MacBook Pro with 4Gb ram, a 3Ghz Intel processor, Nvidia graphics card, a 250Gb solid state disk drive and a WUXGA OLED screen with a 60Hz refresh rate. He also bought an extended warranty, an offical Apple MacPack storage case and some 2Gb SanDisk USB memory sticks.

Then he went home, shagged someone else's wife and solved the murder.

19 Aug 2010

ASP.NET MVC 2 - Force Password Change

How to force the user to change their password when they log on? Here's how. This assumes you're not using the Membership model's User Comments field, because we're going to use it to store a flag. You're going to want to put 'using System.Web.Security;' at the top of your files cos we use the Membership classes a lot.

First define a nice enum somewhere as I'm scared of string literals.

public enum MembershipFlagType

Next we'll put in the code that sets the flag - e.g. in an ActionMethod where the user password is reset:

MembershipUser user = Membership.GetUser(User.Identity.Name);
user.Comment = MembershipFlagType.RequirePasswordChange.ToString();
string newPassword = user.ResetPassword();

Now we define a new Action Filter Attribute to perform a check for the RequirePasswordChange flag:

public class EnforcePasswordPolicy : ActionFilterAttribute
public override void OnActionExecuting(ActionExecutingContext filterContext)
MembershipUser user = Membership.GetUser(filterContext.HttpContext.User.Identity.Name);
if (user.Comment == MembershipFlagType.RequirePasswordChange.ToString())
filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new {controller = "account", action = "changepassword"}));


Then all you have to do is decorate your ActionMethods with the new attribute:

public ActionResult Index(IndexViewModel viewModel)

Don't forget to clear the comment field after the user has changed their password, otherwise they'll be stuck in a loop.

ASP.NET MVC Email Validation Attribute using Regular Expression

You want MVC 2 / 3 to automatically validate user-submitted email addresses? Stick the following class in your project:
using System.ComponentModel.DataAnnotations;

public class EmailValidationAttribute: RegularExpressionAttribute
    public EmailValidationAttribute() : base(@"^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-zA-Z0-9]{1}[a-zA-Z0-9\-]{0,62}[a-zA-Z0-9]{1})|[a-zA-Z])\.)+[a-zA-Z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$") {  }

Then you can use it on your model / model-metadata like this:
[EmailValidation(ErrorMessage="Not a valid Email Address")]
public string Email { get; set; }

NOTE: The regex is STRONG AND GOODLY because I found a definitive source. Scott Gu has a similar solution but for once THE MIGHTY GU HAS LET US DOWN. :p

1. His regex is incomplete and wrong (doesn't allow upper-case first part to the address)
2. His blogpost comments are disabled so I can't let him know about it.


17 Aug 2010

JS Framework Roundup #834543

If you're a jaded Ajax monkey like me you'll have spent an age fighting the likes of Duffle, jSeraph and DOMinic trying to get cross-domain block blending right but with unsatisfactory results, either because of unspeakably low-grade filter belling or just the usual HIB/JSTFU squirrel poll chronlocks. I'll save you the hassle of surfing gitbox, mashStash or dumplocker for a better solution, because I have just been using it and it rocks: Cherub for SquirrelNuts.

We've seen stuff from Slutw4re before, anyone remember Shizbazz Tourette or the mNugget KPlaya extension? Paragons of REST/CHEST, and unlike other Web 4.0 JS hiblibs, they always just "felt right" when you decamped them into whatever framework is your homeboy, from MicMacMock to jQuirk. Even H-Christ got a look in, and that wasn't even at Alpha yet (What's before Alpha? Answers on an eCard...)

Anyway, Cherub is their latest baby, and of course it's GPL'd with the NeoMoose caveats so you're fine on uBert, Chuzzle or Medley but might have problems if you have Bonob:OO up to patch But who's rocking Bonob:OO anymore, anyway? What is this, late march 2009?

But I digress! Okay, so you've decamped Cherub and slipstreamed with Mon2WonTon as usual. Now what? Well, look at the new chops and grooves like so:

var myCherub *= this.Cherub({Cherubim: Seraphim});

I know what you're thinking. Exactly! YAY!

More info here: http://dailyjs.com/

11 Aug 2010

ASP.NET MVC 2 Controller - Use Custom Attribute to intercept call to Action

I was thinking that it would be good to have certain MVC Actions run some common code every time they are called. Naturally we could just call a common method on the first line of our Action methods, but that would not be MVC sexeh. After all, we use special Attributes such as [Authorize] and [HttpPost] to intercept the Action, so why not roll our own?

Microsoft have done the hard work for you - all you have to do is extend an attribute called ActionFilterAttribute (a type of FilterAttribute) and override its calls to the following events as required:

- OnActionExecuting
- OnActionExecuted
- OnResultExecuting
- OnResultExecuted

For example, I wanted to update the User's LastActivity timestamp whenever certain Actions were called, so I created the following:

public class LogUserActivityAttribute: ActionFilterAttribute
public override void OnActionExecuted(ActionExecutedContext filterContext)
... Do your log activity stuff here ...


I could then have that code executed on a per-Action basis like so...

public ActionResult DeleteItem(int id)

HTH! Incidentally, don't forget that instead of this funky MVC apprioach, you can set the Application_BeginRequest method in the global.asax file, which runs at the start of every web request to the application, e.g.

protected void Application_BeginRequest(Object sender, EventArgs e)
... Do your per-request stuff here ...

10 Aug 2010

ASP.NET 4 Session State asp_regsql

Erm, are Microsoft being deliberately obtuse or what? asp_regsql.exe runs a wizard for membership and profile storage but utterly fails to offer a pretty GUI option for creating SQL Session State storage. Noooo, that's hidden away in the command line options. This does the trick:

C:\Windows\Microsoft.NET\Framework\v4.0.30319>aspnet_regsql.exe -ssadd -S yourDbServer -E -sstype c -d yourDatabaseNameHere

2 Aug 2010

Share desktop with other users / view computer remotely

How come I'm the last to find out about Microsoft SharedView, which is free, excellent, and works over the internet like a charm?

Because I've got my head in a bucket, that's why.

Sharepoint 2010 - Disable / Turn off "Check In" / "Check Out" and "Approval" requirements

I found that the requirement to check out my master page and CSS files was becoming a pain, every time having to check them back in and approve them too. Clearly I don't need to do this while I'm developing a site and nobody else is on the system.

I thought there would be some kind of master setting for this in Central Administration or Site Settings, but my investigations suggest this is a per-library thing. Anyhoo, the following instructions worked for me:

• Go to Site Actions / Site Settings and click View All Site Content
• Go to the library that contains the file(s) you wish to edit
• In the Ribbon, select the Library Tools / Library tab
• Click Library Settings in the Ribbon
• Click General Settings / Versioning Settings
• See the settings for Content Approval and Require Check Out

15 Jul 2010

SharePoint 2010 error: site master page setting currently applied to this site is invalid

Ugh, Sharepoint 2010. ANYWAY. If you see this error "site master page setting currently applied to this site is invalid" it's probably cos you need to do the following things:

- Set the UI Compatibility Mode of the MasterPage to 4
- Check in the MasterPage
- Publish the MasterPage

That first bit foxed me. To do this, you have to:

- Site Actions > Site Settings
- Galleries > Master pages and page layouts
- Find your master page, click on it and choose "Edit Properties" from the ribbon
- UI Version > 4

Phun. Is this working for you? Let me know! COMMENT! NO REGISTRATION REQUIRED!

8 Jul 2010

C# delegate reminder

I can't be the only person who forgets how to set up a multicast delegate handler EVERY SINGLE TIME I need one. I guess it just feels messy the way you have to declare the delegate, then create an instance of it, then hook up methods to it, then create an invocation method to call the InvocationList....

Anyway, here's a cheat sheet for a typical ASP.NET page or control implementing a multicast delegate, primarily for myself, but may help someone else, who knows....

public delegate void MyDelegateDef();
public MyDelegateDef MyDelegateInstance = null;
readonly object invocationLock = new object();

private void InvokeMyDelegateInstance()
MyDelegateDef handler;
lock (invocationLock) // Lock to get a thread-safe reference
handler = MyDelegateInstance;

if (handler != null)
foreach (MyDelegateDef delegateMethod in handler.GetInvocationList())

protected override void OnLoad(EventArgs e)

// Run methods that are hooked up to MyDelegateInstance by this point

Then anything that needs to hook up to the delegate does it like so (assumes the delegate is declared in an object instance called myControlInstance):

private void onDelegateEvent()
// Do your stuff here

protected override void OnInit(EventArgs e)

// Registers onDelegateEvent() to be called when MyDelegateInstance is invoked
myControlInstance.MyDelegateInstance += onDelegateEvent;

Okay, so EventHandlers should probably be used in this ASP.NET scenario rather than pure delegates, but the principal is sound!

Incidentally, Jon Skeet is the master at this stuff and you should probably read this and then buy his book.

28 Jun 2010

Sql Server Agent Copy File To remote server on LAN

A little batch file that worked for me, to copy a file from one windows server to another on the same LAN.

NET USE Z: \\nearbyserver\c$ /u:nearbyserver\myuser mypasswd
XCOPY c:\my.csv z:\ /Y

14 Jun 2010

O2 Mobile Broadband Dongle Connection Manager Woe

O2 sucks with my Huawei E160 yada yada. The O2 Connection Manager software just would not connect this morning, with a "Failed To Connect" and "Check your network coverage and user profile" error.

This may be cargo cultish but the solution I found was to:

- Uninstall O2 Connection Manager
- Delete the Cellular Profile from Control Manager / Network Connections
- Restart the machine
- Reinstall O2 Connection Manager by plugging in the dongle
- MOST IMPORTANTLY: Run this patch

Only then did it work again.

25 May 2010

iPhone iPlayer Click To Play Stopped Working

Hi! I LOVE my iPhone 3GS. I also LOVE BBC iPlayer. So imagine my horror when one day I clicked on a radio show to play and it didn't work anymore! I pressed on the "Click to play" triangle, the screen flickered, but the standard Quicktime thingy didn't appear. Nada, zip. Same for TV shows.

At first I thought it was a problem with my wifi but no. Turned out I'd somehow turned off PlugIns in the iPhone browser. To reactivate them:

Go to Settings > Safari > Plug-Ins and turn it back ON :)

21 Apr 2010

SQL Server 2005 - Snapshot Publishing Fun!

I tried to set up Publishing from my local SQL Server but it reckoned that the server name I was using was an alias. It also suggested that I use it's 'proper' server name, which I recognised as being the machine's name from ages ago before I changed it.

Turns out when I ran SELECT @@SERVERNAME , it was returning the OLD name.

The fix was to run this script (found here):

-- Use the Master database
USE master

-- Declare local variables
DECLARE @serverproperty_servername varchar(100),
@servername varchar(100)

-- Get the value returned by the SERVERPROPERTY system function
SELECT @serverproperty_servername = CONVERT(varchar(100), SERVERPROPERTY('ServerName'))

-- Get the value returned by @@SERVERNAME global variable
SELECT @servername = CONVERT(varchar(100), @@SERVERNAME)

-- Drop the server with incorrect name
EXEC sp_dropserver @server=@servername

-- Add the correct server as a local server
EXEC sp_addserver @server=@serverproperty_servername, @local='local'

The server needed a reboot afterwards to make this fix work.

24 Mar 2010

Export / Convert DataTable to Excel (XML format XLS file) in MVC

I see a thousand ASP.NET developers wanting to output an Excel-format document based on a lowly .NET DataTable.

I see people trying to output in CSV and then running into problems with Excel misinterpreting the cell datatype formatting.

I see people wanting to create an Excel Workbook with more than one Worksheet.

So, I present this! A little standalone class that lets you
  • create an Excel document (XMLSS format)
  • add as many Worksheets as you like by simply chucking DataTables at it
  • send it to the browser for download

Usage an an MVC Controller Action:

public void ExportData()
    DataTable dtYourData = YourApp.GetYourDataTable();
    ExcelWorkbookGenerator exGen = new ExcelWorkbookGenerator();
    exGen.AddWorksheet("YourWorksheetTitle", dtYourData);


using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Xml;
using System.Web;

namespace DaddyCode.Utilities
    /// <summary>
    /// ExcelWorkbookGenerator : Generate Excel XML - compatible documents from DataTables.
    /// </summary>
    /// <remarks>
    ///      Author: James McCormack, DaddyCode Ltd
    /// </remarks>
    public class ExcelWorkbookGenerator
        private class Worksheet
            public string Title = "";
            public DataTable Data = null;

            public Worksheet(string title, DataTable dataTable)
                this.Title = title;
                this.Data = dataTable;

        private List<Worksheet> Worksheets = new List<Worksheet>();

        /// <summary>
        /// Add a new Worksheet to the Workbook, based on a DataTable that you provide
        /// </summary>
        /// <param name="title"></param>
        /// <param name="dataTable"></param>
        public void AddWorksheet(string title, DataTable dataTable)
            Worksheets.Add(new Worksheet(title, dataTable));

        /// <summary>
        /// Send the current Workbook to the Web Browser to view or save the file
        /// </summary>
        /// <param name="suggestedFileName"></param>
        public void SendToBrowser(string suggestedFileName)
            HttpContext.Current.Response.ContentType = "application/vnd.ms-excel";
            HttpContext.Current.Response.AddHeader("content-disposition", "attachment; filename=" + suggestedFileName);

        /// <summary>
        /// Generate an Excel-compliant XML Workbook
        /// </summary>
        /// <returns></returns>
        private string getWorkbookXML()
            XmlDocument xDoc = new XmlDocument();
            xDoc.AppendChild(xDoc.CreateNode(XmlNodeType.XmlDeclaration, null, null));

            string strCustomNamespace = "urn:schemas-microsoft-com:office:spreadsheet";

            XmlElement root = xDoc.CreateElement("Workbook");
            root.SetAttribute("xmlns", strCustomNamespace);

            XmlElement styles = xDoc.CreateElement("Styles");

                XmlElement styleBold = xDoc.CreateElement("Style");
                    XmlElement font = xDoc.CreateElement("Font");

                    XmlAttribute xStyleBoldID = xDoc.CreateAttribute("dc", "ID", strCustomNamespace);
                    xStyleBoldID.Value = "dc1";

                    XmlAttribute xFontWeight = xDoc.CreateAttribute("dc", "Bold", strCustomNamespace);
                    xFontWeight.Value = "1";


                XmlElement styleDateTime = xDoc.CreateElement("Style");
                    XmlElement numberFormat = xDoc.CreateElement("NumberFormat");

                    XmlAttribute xStyleDateTimeID = xDoc.CreateAttribute("dc", "ID", strCustomNamespace);
                    xStyleDateTimeID.Value = "dcDateTime";

                    XmlAttribute xStyleNumberFormat = xDoc.CreateAttribute("dc", "Format", strCustomNamespace);
                    xStyleNumberFormat.Value = "General Date";



            // Populate worksheets

            foreach (Worksheet wSheet in Worksheets)
                XmlElement worksheet = xDoc.CreateElement("Worksheet");

                XmlAttribute xSheetTitle = xDoc.CreateAttribute("dc", "Name", strCustomNamespace);
                xSheetTitle.Value = System.Text.RegularExpressions.Regex.Replace(wSheet.Title, "[^a-z0-9 -]", "", System.Text.RegularExpressions.RegexOptions.IgnoreCase);

                XmlElement table = xDoc.CreateElement("Table");

                // Populate header row

                XmlElement header = xDoc.CreateElement("Row");
                XmlAttribute xHeaderStyle = xDoc.CreateAttribute("dc", "StyleID", strCustomNamespace);
                xHeaderStyle.Value = "dc1";

                foreach (DataColumn col in wSheet.Data.Columns)
                    XmlElement headerCell = xDoc.CreateElement("Cell");
                    XmlElement headerData = xDoc.CreateElement("Data");
                    headerData.InnerText = col.ColumnName;

                    XmlAttribute xHeaderDataType = xDoc.CreateAttribute("dc", "Type", strCustomNamespace);
                    xHeaderDataType.Value = "String";


                // Populate data rows

                foreach (DataRow drData in wSheet.Data.Rows)
                    XmlElement row = xDoc.CreateElement("Row");

                    foreach (DataColumn col in wSheet.Data.Columns)
                        XmlElement cell = xDoc.CreateElement("Cell");
                        XmlElement cellData = xDoc.CreateElement("Data");
                        XmlAttribute xCellDataType = xDoc.CreateAttribute("dc", "Type", strCustomNamespace);

                        if (drData[col.ColumnName] == DBNull.Value)
                            cellData.InnerText = "";
                            xCellDataType.Value = "String";
                            switch (col.DataType.Name)
                                case "Single":
                                case "Double":
                                case "Decimal":
                                case "Int16":
                                case "Int32":
                                case "Int64":

                                    cellData.InnerText = drData[col.ColumnName].ToString();
                                    xCellDataType.Value = "Number";

                                case "DateTime":

                                    XmlAttribute xCellStyleID = xDoc.CreateAttribute("dc", "StyleID", strCustomNamespace);
                                    xCellStyleID.Value = "dcDateTime";
                                    if (drData[col.ColumnName] != null 
                                            && drData[col.ColumnName] != DBNull.Value
                                            && (DateTime)drData[col.ColumnName] != DateTime.MinValue)
                                        cellData.InnerText = ((DateTime)drData[col.ColumnName]).ToString("o");  // ISO 8601 DateTime String Format
                                    xCellDataType.Value = "DateTime";

                                case "Boolean":

                                    cellData.InnerText = (bool)drData[col.ColumnName] ? "1" : "0";
                                    xCellDataType.Value = "Boolean";


                                    cellData.InnerText = drData[col.ColumnName].ToString(); // XmlElement.InnerText escapes reserved XML characters automatically
                                    xCellDataType.Value = "String";




            StringWriter swOut = new StringWriter();

            return swOut.ToString();

If you're reading this and know a better way to do this sort of thing - PLEASE LET ME KNOW. I was driven to this because of the crap documentation on the web. I only achieved this limited success by reverse-engineering an existing Excel doc and latterly discovering the MS XML Spreadsheet Reference. Why they don't tell you that the XMLSS DateTime format is ISO 8601, I don't know...

21 Jan 2010

ASP.NET GridRow - Get Cell when you don't know the index - using HeaderText, DataField, SortExpression etc.

Sometimes you want to reference a cell in a gridview row, but you don't know its ordinal index, so Row.Cells[x] is no good for you. In this circumstance it would be nice to say something like "just get me the cell from the column with the HeaderText value 'Price'".


public static int GetCellIndexByFieldHandle(this GridView grid, string fieldHandle)
int iCellIndex = -1;

for (int iColIndex = 0; iColIndex < grid.Columns.Count; iColIndex++)
if (grid.Columns[iColIndex] is DataControlField)
DataControlField col = (DataControlField)grid.Columns[iColIndex];
if ((col is BoundField && string.Compare(((BoundField)col).DataField, fieldHandle, true) == 0)
|| string.Compare(col.SortExpression, fieldHandle, true) == 0
|| col.HeaderText.Contains(fieldHandle))
iCellIndex = iColIndex;
return iCellIndex;


void myGrid_RowDataBound(object sender, GridViewRowEventArgs e)
if (e.Row.RowType == DataControlRowType.DataRow)
TableCell cellPrice = e.Row.Cells[e.Row.GetCellIndexByFieldHandle('Price')];

The method works for column HeaderText, DataField and SortExpression, so you should always have a way to grab hold of that cell. One caveat - it can't reference AutoGenerated columns. They have to be defined in the GridView template a la:

<asp:BoundField DataField="Price" />


<asp:TemplateField HeaderText="Price">
<asp:Label ID="lblPrice" runat="server" />

A JQuery function to set the value of all input controls which have an ID containing a given string

function setAll(idFilter, value) {

$('input[id*=' + idFilter + ']').each(function() {

JQuery has some superduper wildcard attribute selectors like *= (contains), ^= (begins with) and $= (ends with). More here.

19 Jan 2010

iPhone on Orange - Cellular Data Network problem

I have an iPhone 3GS on the Orange UK network. It stopped being able to access the internet by any means other than wifi. 3G, Edge, GPRS - none of it worked.

I phoned Orange support - they said that a Carrier Settings update had screwed up the configuration. Normally, you can manually configure your phone in the Settings > General > Network > Cellular Data Network menu, but the Orange carrier settings update restricts your access to this.

To sort the problem, I had to do a full restore, allow the iPhone to reconnect with Orange via iTunes, but then deny the requests to update the Carrier Settings. By doing this, the Cellular Data Network menu was restored and I was able to set the APN correctly to orangeinternet (no user/pass required). Now internet could be accessed by the cellular data network again.

After a few days I did another sync with iTunes and this time when the carrier settings prompt appeared, I allowed it to update. Again, the Cellular Data Network menu was removed and although the phone continued to be able to access the internet without WIFI, I was annoyed to discover that the phone seemed to have had its MMS settings removed - it had even removed the option to send a photo as an MMS.

Before I resorted to doing another restore, I tried turning the phone off and on again - hey presto, MMS came back and all was okay again. Phew!

3 Jan 2010

Addictive Drums + Reaper + Roland TD3 VDrums + MIDI UM-1G USB

Maybe you're a daft sod like me that doesn't read the instructions, but I had a few hurdles setting up this combo. So here's some tips.

1. Attach the UM-1G unit MIDI OUT cable to the MIDI OUT socket on the TD-3.
2. Install the UM-1G drivers in Windows and then attach the UM-1G USB cable.
3. Download ASIO4ALL v2 and install.
4. Install Addictive Drums.
5. Fire up Reaper.
6. In Preferences > Audio > MIDI Devices, Enable the UM-1G.
7. In Preferences > Audio > Device, Select Audio System : ASIO and choose Asio4All as the Driver.
8. Create a new track and arm it. Turn record monitoring on. Select MIDI > UM-1G > All Channels as the Input source. Enter FX, add the Addictive Drums VSTi.

At this point you should be getting sound out of your drums.

If you can't find the Addictive Drums instrument, maybe you should locate the vst file and place in the Reaper VST path?

If the MIDI map is wrong for your kit, go into the AD VST and click the "?" button - open the Map Window. Under Map Preset, choose Roland > TD-3. You can do further custom mapping by creating a Reaper JS file in the Program Files / Reaper / Effects folder. I found a couple of useful ready-made ones here.

I now realise that ASIO4ALL grabs the sound device and stops other apps like Windows Media Player from using it - annoying when you want to jam. I don't know a way round it, can anyone shed some light? At the moment it's okay cos I've hooked up my GuitarPort and am using it's ASIO driver instead of ASIO4ALL.

Comments / Questions appreciated!

LogiTech S530 Keyboard + Mouse For Mac - On Windows

Quick one - if like me you're using a Mac with Bootcamp and wondered if your Logitech S530 keyboard/mouse combo would work okay with Windows (and Win 7 in my case) - the answer is yes. It works fine out of the box.

Even better news is it works with the S510 drivers (SetPoint suite) if you download them off the Logitech site here.

When you do that all the cool shortcut buttons work as intended - volume control, quicklaunch etc.

Hope that helps someone!
If I helped you out today, you can buy me a beer below. Cheers!