Hello Afrihost.

Posted on June 12, 2014

I received an sms today asking me to rate your feedback over a support ticket I logged via email the other day. I don’t mind rating your feedback, but I do mind the communication channel you’re taking.

You have a few ways you can choose to contact me.

1) you could phone me
2) you could sms me
3) you could email me.

#1 is a terrible idea. A phone call is a relatively intrusive process and it demands my full attention. I have to stop what I’m doing and take a call. If I’m at work or in a meeting its terribly inconvenient especially for something as simple as giving you a number.

#2 is what you’re currently doing. I think its a close second on the scale of terrible ideas. You send me 2 sms’ (for some reason not just 1) and then I have to pay money in order to give you feedback on how you did helping me when i was using your services that I’m paying you money for.

On top of that, an sms is almost as intrusive as a phone call. It beeps directly at me wherever in the world i am. Friends sms me. Family sms me. My hosting company doesn’t need to sms me, especially not for something as mundane as providing you feedback. And they really don’t need to sms me twice, one arriving milliseconds before the other.

#3 email. This is a great idea. Its a relatively informal, and unobtrusive way of communicating. You can send me an email and I don’t receive beeps, buzzes and am not otherwise interrupted. Its pretty much the primary method of communication between myself and yourself anyway, and its perfect for the kind of low importance stuff we’re dealing with here. Its the standard method of communicating between a business and its customers and I would really urge you to alter your sms feedback policy and use email instead, especially since the support ticket in question was logged via email / web anyway.

At the end of the day you’re essentially spamming your customers. I doubt many of them respond, so you’re spamming as many people as you can. Now spam is one of those things that people no the internet have learnt to deal with. I get plenty of emails every day that are sort of low priority very similar to your feedback request. Its almost expected and its almost universal. But spamming my phone is essentially jumping up a level, and its a level filled with not nice people. Its the mos-eisly of marketers

In short scooters pizza spams my phone and i now hate them. I like you guys. Don’t be scooters.

Sticky or non-responsive keys on a Microsoft Comfort Curve 2000

Posted on February 14, 2013

I have a microsoft curve 2000 keyboard
I actually have a few of them.
They’re great, cheap and comfortable.
The multimedia buttons just “work” which for me is a change compared to some of my previous keyboards. In short I love this keyboard, despite the one giant annoying niggle.

On every single one of them, random keys sometimes just stop working.
And its been bugging the hell out of me.
I’ve popped the keys off, cleaned the hell out of it, put it back together and sometimes it would start working again, but sometimes it wouldn’t.

So eventually I got so annoyed I got a screw driver and removed all of the screws from it, and popped it open. If you do this you’ll see a rubber sheet that the plastic keys hit. Remove the rubber and you’ll see 3 plastic sheets with circuits drawn on them.

Try to seperate these sheets as much as possible (careful, they’re joined in one or two places) and run your hand between the sheets over and around the non-working key. Remember there are three sheets so you’ll have to do it at least twice.

Then pop the rubber back on and test the key. So far every time I’ve dealt with a non-responsive key this has fixed it. I’m guessing some sort of static buildup or something must happen between the plastic sheets. Crappy design, but easy fixed.

Lucene.Net Phrase Suggestion

Posted on March 13, 2012

This post is all about lucene and lucene.net. Lucene is an awesome project, but it can be very confusing. Bert Williams has an awesome set of tutorials on how to get lucene up and running. I used his tutorials to get my basic lucene setup working, and if you’ve never heard of lucene before I recommend checking out his posts first!

Lucene.net is a dotnet port of the original Lucene project which was coded in java. Its a very quick, very awesome full text search index. It comes jam packed full of features, but obviously being a port, it lags a bit behind the original java version. One of the very cool things in Lucene is the ability to store an item in the index, and then break it down into “shingles”, ie different combinations of words and phrases. That way if someone does a search for a phrase “eye or londor”, it might find nothing, but can quickly suggest the phrase “eye of london” ala google.

Unfortunately, the shingle filter is one of the items that the lucene.net engine doesn’t have (yet). This means lucene.net cant break a sentence into its various phrases and store them in an index. You can create a spell index based on single words, but the phrase suggestions won’t work.

However, any lucene indexes created by the project can be read by either the java or the dotnet port. This means you can create your indexes in lucene.net and then write a small java app to create shingles of those terms into a temporary index. This temporary index can then be stored back in your main lucene.net index and all you have to do is search that index for suggestions to come up. Which is exactly what I did.

This is the code I used to do that. To get this to work, follow the steps laid out in Bert Williams’ tutorials above, until you have completed the steps laid out in “alternatives and did you mean” which is the fourth or fifth article in the series. Once you’ve set up a spelling index with single words in it, stop and run my code. This will break down the descriptions into shingles and add those shingles into your existing spelling index. From there on, continue as normal.

Ok, so first off here is the java code which creates a separate, temporary, shingle index.

/**
 * @(#)DirectorySearchShingle.java
 *
 * DirectorySearchShingle application
 *
 * @author
 * @version 1.00 2011/2/16
 */
 import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Date;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.shingle.ShingleAnalyzerWrapper;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.FilterIndexReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Collector;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;


import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.index.TermEnum;

public class DirectorySearchShingle {

	 public static void main(String[] args)
	 {
	 	try
	 	{
	 	 Analyzer temp = new WhitespaceAnalyzer();
		 Analyzer analyzer = new ShingleAnalyzerWrapper(temp, 4);
		 shingleThings(analyzer);
	 	}
	 	catch(Exception ex)
	 	{

	 	}
	 }

	 public static void shingleThings(Analyzer analyzer) throws Exception
	 {
	 	shingleDirectory(analyzer);
	 }

	 public static void shingleDirectory(Analyzer analyzer) throws Exception
	 {

		//this is the lucene index that i have already imported stuff into.				
	 	String dirIndexPathFrom = "C:\\LunceneIndex\\Directory";
	 	
	 	//this is where im storing my shingle results
	 	//this is a temporary index, i will later take this index and merge it with my already existing 
	 	//single phrase index => http://www.devatwork.nl/articles/lucenenet/alternatives-did-you-mean-lucenenet/
	 	String dirIndexPathTo = "C:\\LunceneIndex\\ShingleDirectory";
	 	
	 	//this is a list of fields in the index i want to break down into every possible combination.
	 	//so if the original field contains "the cat jumped"
	 	//the shingles are 
	 	//=> the cat
		//=> cat jumped
		String[] fieldsToIndex = {"Company" };

	 	setUpSearcher(analyzer, dirIndexPathFrom, dirIndexPathTo, fieldsToIndex);
	 	showTerm(dirIndexPathTo);
	 }

    public static void setUpSearcher(Analyzer analyzer, String indexPathToReadFrom, String indexPathToWriteTo, String[] fieldsToImport) throws Exception {
    	try
    	{

    	    //Directory dir = new RAMDirectory();
    //IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig(TEST_VERSION_CURRENT, analyzer));
    IndexWriter writer = new IndexWriter(FSDirectory.open(new File(indexPathToWriteTo)), analyzer, true, IndexWriter.MaxFieldLength.LIMITED);

		IndexReader r = IndexReader.open(FSDirectory.open(new File(indexPathToReadFrom)), true); // only searching, so read-only=true

		int num = r.numDocs();

		for ( int i = 0; i < num; i++)
		{
    		if ( ! r.isDeleted( i))
    	{
	        Document d = r.document( i);

	        for (int jj = 0; jj < fieldsToImport.length; jj++)
	        {
    		    Document doc = new Document();
				doc.add(new Field("word",  d.get(fieldsToImport[jj]),  Field.Store.YES,Field.Index.ANALYZED));
				writer.addDocument(doc);
	        }

    	}
		}

		r.close();
		writer.close();
		System.out.println(num);

    	}
    	catch(Exception ex)
    	{

    	}
  }
  }

Now that I have created my temporary shingle index, I run some c# code in order to add these shingles into my spelling index.

static class importLuceneShingles
   {
       public const System.String F_WORD = "word";
       private static readonly Term F_WORD_TERM = new Term(F_WORD);
 
       public static void importShingles(String indexWritePath, String indexReadPath)
       {
           IndexWriter writer = new IndexWriter(indexWritePath, new WhitespaceAnalyzer(), IndexWriter.MaxFieldLength.UNLIMITED);
           IndexReader r = IndexReader.Open(indexReadPath, true); // only searching, so read-only=true
           IndexSearcher projectTenderTerms = new IndexSearcher(indexWritePath);
 
 
           var temper = r.Terms();
           int i = 0;
 
           while (temper.Next())
           {
               i++;
               if (i % 100000 == 0)
               {
                   //Console.Clear();
                   Console.WriteLine(i);
               }
               String word = temper.Term().Text().ToLower().Trim();
 
 
               if (projectTenderTerms.DocFreq(F_WORD_TERM.CreateTerm(word)) > 0)
               {
                   // if the word already exist in the gramindex
                   continue;
               }
 
               int len = word.Length;
               if (len < 3)
               {
                   continue; // too short we bail but "too long" is fine...
               }
 
               // ok index the word
               Document doc = CreateDocument(word, GetMin(len), GetMax(len));
               writer.AddDocument(doc);
           }
 
           r.Close();
           writer.Optimize();
           writer.Close();
           Console.WriteLine("all the imports are complete, hope your face isn't broken");
 
 
       }
 
       private static int GetMin(int l)
       {
           if (l > 5)
           {
               return 3;
           }
           if (l == 5)
           {
               return 2;
           }
           return 1;
       }
 
       private static int GetMax(int l)
       {
           if (l > 5)
           {
               return 4;
           }
           if (l == 5)
           {
               return 3;
           }
           return 2;
       }
 
       private static Document CreateDocument(System.String text, int ng1, int ng2)
       {
           Document doc = new Document();
           doc.Add(new Field(F_WORD, text, Field.Store.YES, Field.Index.NOT_ANALYZED)); // orig term
           AddGram(text, doc, ng1, ng2);
           return doc;
       }
 
       private static void AddGram(System.String text, Document doc, int ng1, int ng2)
       {
           int len = text.Length;
           for (int ng = ng1; ng <= ng2; ng++)
           {
               System.String key = "gram" + ng;
               System.String end = null;
               for (int i = 0; i < len - ng + 1; i++)
               {
                   System.String gram = text.Substring(i, (i + ng) - (i));
                   doc.Add(new Field(key, gram, Field.Store.NO, Field.Index.NOT_ANALYZED));
                   if (i == 0)
                   {
                       doc.Add(new Field("start" + ng, gram, Field.Store.NO, Field.Index.NOT_ANALYZED));
                   }
                   end = gram;
               }
               if (end != null)
               {
                   // may not be present if len==ng1
                   doc.Add(new Field("end" + ng, end, Field.Store.NO, Field.Index.NOT_ANALYZED));
               }
           }
       }

The importShingles method takes two arguments, indexWritePath which represents your existing spelling index and indexReadPath which is the path to the index we have just created in our java application. Once this method has run thats it, you’ve succesfully stored all of your shingles into your spelling index. Your suggestions will now work with single words and phrases. Just remember to re-run the spelling and importing whenever you add a new object into your main index.

The best aren’t everywhere in South Africa

Posted on January 18, 2012

I’ve just finished reading Rework, a brilliant book I think every coder should read by 37Signals. The book is just generally badass, but one thing struck me in particular. The chapter is entitled “The Best Are Everywhere”

Basically its a page and a half case study on why they hire staff on skill and aptitude and not on location. They’ve created an awesome company comprised of staff literally scattered around the world. But in todays online world, its really not that big a deal, between online repo’s and build servers, VM’s, IM’s, email, phone and video chat they somehow make do.

Scott Hanselman has written several posts about his experiences as a remote employee. He’s actually got an entire section of his blog dedicated to remote work and how he gets around some of the issues he faces. It makes for interesting reading, but it also highlights the fact that he manages just fine.

Now if remote employees work fine for small companies like 37Signals and it also works fine for big companies like microsoft, what the hell is stopping the average company in south africa from following? I don’t get it at all.

It cuts out a commute, means I can work flexi time, in comfort and at home. At the end of the day it saves me money. Work doesn’t need a giant office, parking space etc etc etc. It saves work money.Surely its a win win?

But every time you go to career junction or get an email from a recruiter or see a job ad in the paper they need you to come in and fill a chair. Its things like this that make me convinced that some south african IT companies just don’t get the concept of moving forward.

You don’t build a kickass tech company by taking new tech and bogging it down with old school management techniques and bureaucracy. You don’t get really inventive, creative and fresh ideas by forcing people to wear a suit and tie and come in from 9-5 to sit in a box. And you don’t find the best people by limiting your field of vision to what you see outside your window.

 

A better way of working with HTTPContext.Session in MVC

Posted on January 18, 2012

One of the problems I’ve been having is passing session in and out of my controllers, helpers, and even models in some cases. Its been messy, and thats before I even thought about how it would work when it came to testing…

This week I sat down and came up with what I think is a pretty clean and neat implementation when it comes to dealing with session.

As always, it starts with an interface.

public interface ISessionCache
{
    T Get<T>(string key);
    void Set<T>(string key, T item);
    bool contains(string key);
    void clearKey(string key);
    T singleTon<T>(String key, getStuffAction<T> actionToPerform);
}

Now it all looks pretty standard, except for that last method. That last method, singleTon, was created simply because I’m sick and tired of using session in my get properties or wherever using the singleton pattern. Using generics and a delegate, I have made a base class to do my singleton checks for me.

Here is my aptly named delegate, its basically any method that returns T.

public delegate T getStuffAction<T>();

I marked the base class as abstract and also marked all of the ISessionCache members as abstract except for the singleTon method. Like i said, the methods nothing fancy, it just saves me some keystrokes, and cleans up my code a bit.

public abstract class BaseSessionCache : ISessionCache
{
    public abstract T Get<T>(string key);
    public abstract void Set<T>(string key, T item);
    public abstract bool contains(string key);
    public abstract void clearKey(string key);
    public virtual T singleTon<T>(String key, getStuffAction<T> actionToPerform)
    {
        if (!contains(key))
        {
            Set<T>(key, actionToPerform());
        }
        return Get<T>(key);
    }
}

Next is an in memory implementation I can use for testing purposes. It basically adds and gets stuff out of a dictionary.

public class InMemorySessionCache : BaseSessionCache
    {
        Dictionary<String, Object> _col;
        public InMemorySessionCache()
        {
            _col = new Dictionary<string, object>();
        }

        public T Get<T>(string key)
        {
            return (T)_col[key];
        }

        public void Set<T>(string key, T item)
        {
            _col.Add(key, item);
        }

        public bool contains(string key)
        {
            if (_col.ContainsKey(key))
            {
                return true;
            }
            return false;
        }

        public void clearKey(string key)
        {
            if (contains(key))
            {
                _col.Remove(key);
            }
        }
    }

And finally is my actual HTTPContext.Session implementation that I use in my live site.

public class HttpContextSessionCache : BaseSessionCache
{
    private readonly HttpContext _context;

    public HttpContextSessionCache()
    {
        _context = HttpContext.Current;
    }

    public T Get<T>(string key)
    {
        object value = _context.Session[key];
        return value == null ? default(T) : (T)value;
    }

    public void Set<T>(string key, T item)
    {
        _context.Session[key] = item;
    }

    public bool contains(string key)
    {
        if (_context.Session[key] != null)
        {
            return true;
        }
        return false;
    }
    public void clearKey(string key)
    {
        _context.Session[key] = null;
    }
}
 

Once again, pretty standard stuff.

Now lets see it all in action.

Public class FunController : Controller

{
    private ISessionCache _cache;

    public String SomethingInSession
    {
        get
        {
            //the traditional singleton pattern
            if (!_cache.contains("somekey"))
            {
                _cache.Set<String>("somekey", "somevalue");
            }

            return _cache.Get<String>("somekey");
        }

        set
        {
            _cache.Set<String>("somekey", value);
        }
    }

    public String SomethingElseInSession
    {
        get
        {
            //using the singleton method, saves you some lines of code.
            return _cache.singleTon<String>("somekey", () => "somevalue");
        }

        set
        {
            _cache.Set<String>("somekey", value);
        }
    }

    public FunController(ISessionCache cache)
    {
        _cache = cache;
    }

    public ActionResult Index()
    {
        //check if session contains someKey
        if (_cache.contains("someKey"))
        {
            //if not, add a list of string
            _cache.Set<List<String>>("someKey", new List<string>() { "im in session", "me too" });
        }

        //somekey is a strongly typed list of string
        var t = _cache.Get<List<String>>("someKey");

        //clear it out of session
        _cache.clearKey("someKey");
        return new EmptyResult();
    }
}
 

Now you can hook the interface up to your dependency injection and inject session into your controllers, helpers and whatever else you need without worrying about sacrificing your testability. Just wire up your test methods to use the in memory implementation and off you go. Yay…

Extending the ActionLink Helper in MVC

Posted on January 18, 2012

The ActionLink Helper is used to generate anchor links in your views in MVC. The problem I’ve been having is that if the action on a controller gets renamed, or a controller gets moved to a new Area, its really hard to find all of the action links that reference the action or controller and update them.

With that in mind, I created a small helper object and added an extension overload to the ActionLink method to solve this problem.

To start off, here is my ActionLink overload

public static HtmlString ActionLink(this HtmlHelper helper, LinkData data)
        {
            return System.Web.Mvc.Html.LinkExtensions.ActionLink(helper, data.LinkText, data.Action, data.Controller, data.routeValues, data.htmlAttributes);
        }

As you can see this takes in a LinkData object which looks like this

public class LinkData
    {
        public String Controller { get; set; }
        public String Action { get; set; }
        public RouteValueDictionary routeValues { get; set; }
        public Dictionary&lt;string,object&gt; htmlAttributes { get; set; }
        public String LinkText { get; set; }

        public LinkData()
        {
            this.routeValues = new RouteValueDictionary();
            this.htmlAttributes = new  Dictionary&lt;string,object&gt;();
        }

        public LinkData(String key,Object data) : this()
        {
            AddRouteData(key, data);
        }

        public LinkData(String areaName)
            : this("area", areaName)
        {

        }

        public void AddRouteData(String key, object data)
        {
            routeValues.Add(key, data);
        }

        public void AddHtmlAttribute(String key, object data)
        {
            htmlAttributes.Add(key, data);
        }

        //over ride equals method for testing
        public override bool Equals(object obj)
        {
            if (obj is LinkData)
            {
                return this.Equals((LinkData)obj);
            }

            return false;
        }

        public bool Equals(LinkData obj)
        {
            if (obj.Action != this.Action)
            {
                return false;
            }

            if (obj.Controller != this.Controller)
            {
                return false;
            }

            return true;
        }
    }

This LinkData object represents the information you need to generate an action link. Its got a controller name, an action name, the display text for the actual anchor. You can add html attributes to it, and its even got a route dictionary in case you need to pass things like area name etc in it.

Now I simply generate a helper class to generate return me an instance of LinkData. In here I put the string values for the controllername, the area name, the action name etc. This is the only place this information is entered so if it ever changes I only update it here.

public static partial class LinkDataHelper
    {

        public static LinkData PageInArea()
        {
            //return a link to an action in an area
            return linkDataLink("some linkText", "ActionName", "ControllerName", "AreaName");
        }

        public static LinkData HomePage()
        {
            //return a link with no area
            return linkDataLink("Home", "Index", "Home");
        }

        private static LinkData linkDataLink(String linkText, String action, String controller)
        {
            return new LinkData() { LinkText = linkText, Action = action, Controller = controller };
        }

        private static LinkData linkDataLink(String linkText, String action, String controller, String area)
        {
          return new LinkData(area) { LinkText= linkText, Action = action, Controller = controller};
        }
    }

Finally on a view, to create a link to the home page I simply type

@Html.ActionLink(LinkDataHelper.HomePage())

and the link to the page in the area looks like so

@Html.ActionLink(LinkDataHelper.PageInArea())

I have also added some overloads to the link helper allowing me to send in the link text I want to display, and for edit links I send in a value that gets added to the routedictionary with the appropriate key. This small amount of effort has paid off wholesale when it comes to moving controllers, renaming actions, stopping typo’s etc.

 

If you have any comments of suggestions feel free to contact me or leave your thoughts below.

Niggles with ViewData and the Html.DropDownList

Posted on January 18, 2012

Just a quick post, to serve as reference to my future self.

There is a weird niggle that catches me at least once every 6 weeks when using ViewData to populate an Html.DropDownList in a view. Here is the simple example that cost me 30 minutes this morning

List<SelectListItem> months = new List<SelectListItem>();

for (int i = 1; i < 13; i++)
{
    months.Add(new SelectListItem()
    {
    Text = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(i),
    Value = i.ToString(),
    Selected = (i == defaultMonth)
    });
}

ViewData["months"] = months;

As you can see, nothing special, I’m looping through months and adding them to a list of SelectListItems that get put into ViewData. There is one variable not shown, an int called defaultMonth that is going to be the lists selected item. On my View I have the following code.

@Html.DropDownList("months", (IEnumerable<SelectListItem>)ViewData["months"])

Its a simple one liner that creates a drop down based on my list, nothing special about it, we’ve all done something similar hundreds of times. Except that this will never ever ever show the list with your selected value, it will always show the first item. If you look at the values in debug as they are getting put into viewdata, everything is correct, and the value you want is the only value with selected set to true.

But the view will ignore it. Until you change the name of the Html.DropDownList so that it DOESN’T match the ViewData key you’re using to populate the list.

@Html.DropDownList("monthsDD", (IEnumerable<SelectListItem>)ViewData["months"])

Thats my working code….

*sigh*