About ribbon and user interfaces

There’s been some discussion on the blogosphere lately, especially after Patrick Smacchia’s posts on new interface for NDepend. Well, I’ve already said, that as much as I love this tool, it’s current interface leaves much to be desired. It’s a good thing that finally they are doing something about it, and in the direction I talked about in my first post (similar to Visual Studio). Only thing that surprises  me is that by default NDepend will have ribbon, but you (fortunately) will be able to go to options and change it to good old menus and toolstrips.

I’m not going to comment on NDepend team’ choice, instead I’d like to concentrate on other thing – is ribbon really something we should go for? I get it, Office 2007 has ribbon, so finally we have something new and fresh in stiff winforms world, so everyone eagerly wants to play with it. But I don’t really think ribbon is like any other winforms control. I think it’s simply not general purpose, like a button, combobox, or even toolstrip.

There are several reasons why Microsoft put ribbon in office. The most apparent one is target group. What is ribbon anyway? It’s a toolstrip with tabs and really big icons. The reason why Microsoft decided to build office’ user interface with it (the way I see it) is to make it easier for average secretary for whom computer is voodoo to use it. Instead of menus hiding option everything is right in front of her eyes, labeled, and with big shiny icons. That certainly makes it easier for starters to use it.

For advanced users however result is opposite. Advanced users do most of their work with keyboard shortcuts, and don’t care about big icons and descriptive labels under them. What they rather care about is: this big thing on top of my screen eats up lot of space on my monitor. This is especially important in tools where you want to have multiple windows open at once, and every pixel is worth gold.

There’s a reason why Microsoft didn’t put ribbon in VS 2008, and (again, the way I see it) that is exactly this: different target group. Visual Studio is targeted for advanced users who don’t care about big icons and who most of the time use keyboard shortcuts.

Another confusing thing about ribbon is its contextual nature. Like in Word when you click on a table, a tab on a ribbon pops out that lets you select options specific to tables, like add a row, add a column etc. That’s great, but two things first: There is absolutely NO reason why you couldn’t do it with menus and tools strips, and: in case of ribbon, it’s not an option: it’s necessity. Simply ribbon takes so much space that having those all tabs and groups open all the time would eat up space on whole normal monitor. or would require like 3 levels of tabs, that would be even more confusing than toolstrips. And the reason why (almost) no one is doing this with toolstrips and menus is that “Why hide it if there’s enough room for all of it?”.

I personally thing that over time we’ll see some applications with ribbon, as some applications can really improve user experience by leveraging ribbon, but vast majority of those will be targeted for non-computer people. I see ribbon simply as new toy people want to play with. It’s like, in one episode of Dot Net Rocks, if I recall correctly, it probably was about WPF, Carl’s and Richard’s guest said that back in VB3.0 days there were applications that had red forms, simply because they could. I guess what happens with ribbon now, is a similar thing.

Updating Controls in Windows Forms

How often do you find yourself writing code like this:

string[] files = GetFiles(path);

filesListView.BeginUpdate();

for (int i = 0; i < files.Length; i++)

{

    //possibly something more

    filesListView.Items.Add(new ListViewItem(files[i]));

}

filesListView.EndUpdate();

You suspend control with BeginUpdate() in order not to repaint itself with every change you make, then do several updates to it, and then you let go of it, with EndUpdate(), so that it could repaint itself to reflect all the changes. Easy and simple isn’t it? So, what’s the problem? I can see at least two:

  1. Readability
  2. It’s error prone.

I created only very few lines of code for this example, but you have to look into the code to find the portion between BeginUpdate, and EndUpdate. It’s not apparent at the first glimpse of an eye, as it should be. I think that you should be able to immediatelly tell, what portion of the code happens when the control is suspended, and what not. With this code, you first have to parse it to find BeginUpdate(), and then matching EndUpdate.

And hey, have you ever forgotten to add EndUpdate, at the end of similar code snippet? It, compiles, you don’t even get a warning, but it works far from what you would like it to.

To overcome those constraints I decided  to create a simple helper class that would allow me to change code above to something like:

string[] files = GetFiles(path);

using (new Update<ListView>(filesListView))

{

    for (int i = 0; i < files.Length; i++)

    {

        //possibly something more

        filesListView.Items.Add(new ListViewItem(files[i]));

    }

}

//do something else if you need to

Isn’t it much cleaner? Immediately from looking at the code you can see which portion is in Update block. You also don’t have to worry

that  you forget to call EndUpdate() – it will be done for you.

If you ever played with WinForms controls, you certainly noticed that not every control has Begin/EndUpdate pair of methods. I never actually checked, but I assumed that for those who do have them, those methods come from some base class, or maybe interface. I was greatly surprised to discover, that neither of those assumptions was true. There simply are controls that do have those methods, and those that do not, period. I guess that’s not the best solution, and there should be and interface like IUpdateable, ISupportsUpdate or something.

Lack of that interface made me use Update<T> where T:Control instead of Update<T> where T:IUpdateable, so now you can pass in any control, even one, that has no Begin/EndUpdate methods implemented.

So what now? Throw NotImplementedException? That was my initial thought, but it means that everything would compile, and you would get exception during runtime which is something I really dislike. This leaves us with another option: if given control doesn’t implement said methods do nothing. It may seem the most reasonable option but it promises consistent behavior, and doesn’t provide it.

Imagine code like this:

using (new Update<Label>(myLabel))

{

    for (int i = 0; i < 10; i++)

    {

        myLabel.Text += i.ToString();

        Application.DoEvents();

        Thread.Sleep(500);

    }

}

Label, obviously doesn’t have Begin/EndUpdate, but it’s a Control and this code would compile. Someone would expect that for 5 seconds label’s text stays unchanged, and after that it updates, all the changes in one go. Well, he/she would be bitterly disappointed by the outcome, since instead of one change, we would have 10 of them. It’s not what we wanted, it’s not what we ought to have, since it means that we get inconsistent behavior.

So what now? I poke around a little bit with Reflector, and I discovered, that Control class has internal method BeginUpdateInternal, and consequently EndUpdateInternal. That’s what we wanted! Althought it’s a hacks, because we need to use reflection to call internal method from outside of its assembly it’s the best option we have in this situation. Now we can call public methods for controls that have them, and Control’s internal classes for other ones.

Still, I’m not completely satisfied with this solution. I shouldn’t be able to do it for Label, TextBox or any other control that is not meant to be updated this way, but to do this, all those controls that do support this behavior should all be marked with Interface, and that’s something that only BCL team can do.

Final code for Update<T> looks like this:

public class Update<T> : IDisposable where T : Control

{

    private T _control;

    private Type _type;

    private bool _hasPublicUpdate = false;

 

    public Update(T control)

    {

        _type = control.GetType();

        MethodInfo beginUpdate = _type.GetMethod("BeginUpdate");

        if (beginUpdate == null)

        {

            _type = typeof(System.Windows.Forms.Control);

            beginUpdate = _type.GetMethod("BeginUpdateInternal", BindingFlags.NonPublic | BindingFlags.Instance);

        }

        else

        {

            _hasPublicUpdate = true;

        }

        _control = control;

        beginUpdate.Invoke(_control, null);

    }

 

    public void Dispose()

    {

        MethodInfo endUpdate = null;

        if (_hasPublicUpdate)

        {

            endUpdate = _type.GetMethod("EndUpdate");

            endUpdate.Invoke(_control, null);

        }

        else

        {

            endUpdate = _type.GetMethod("EndUpdateInternal", BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(bool) }, new ParameterModifier[] { new ParameterModifier(1) });

            endUpdate.Invoke(_control, new object[] { true });

        }

        _control = null;

        GC.SuppressFinalize(this);

    }

}

Technorati tags: , , , , , ,