Friday, 13 June 2008

Creating a Piano Roll in WPF Part 3 - Data Binding

I have posted a couple of times on creating a Piano Roll control in WPF. (Part 1 and Part 2). Today I want to take a slight digression and ask whether this could be accomplished more elegantly using a data binding approach. I went to an excellent presentation by Josh Twist on WPF and data binding yesterday in which he claimed that if you are not using data binding in WPF, you a probably missing something.

So I decided to experiment. First, rather than setting a property on our PianoRoll control, we simply set the DataContext of our Window and let it propagate down:

MidiFile midiFile = new MidiFile(openFileDialog.FileName);
this.DataContext = midiFile.Events;

Now we can bind a plain old ListBox to that property. Unfortunately because the MidiEventCollection is not simply a list of events but a list of lists (one per track), I need to use the slightly odd .[2] path binding syntax to get at the events for the third track (which is typically the first one with any notes on for multi-timbral MIDI files).

<ListBox Grid.Row ="1" x:Name="PianoRollListBox" ItemsSource="{Binding Path=.[2]}" />

Here's what it looks like:

Binding a listbox to MIDI

Now it is very cool that we can get a textual representation so easily, but of course this is not the look we are after. We need to change the ItemsPanel which by default is a StackPanel and make it a Canvas instead:

<ListBox Grid.Row ="1" x:Name="PianoRollListBox" 
         ItemsSource="{Binding Path=.[2]}">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

Visually, things get worse...

Binding to a Canvas

So now we want to get back to having rectangles representing MIDI events. First we need to do some sizing and scaling on our ItemsPanel Canvas to get things roughly the right size. Then we will create a DataTemplate for the rectangle itself. The only property we bind to is the NoteLength, which is used to set the rectangle's Width.

<ListBox Grid.Row ="1" x:Name="PianoRollListBox" 
         ItemsSource="{Binding Path=.}" 
         ItemContainerStyle="{StaticResource PianoRollItemContainerStyle}">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas Height="1280" Width="4000">
                <Canvas.RenderTransform>
                    <ScaleTransform ScaleX="0.2" ScaleY="10" />
                </Canvas.RenderTransform>
            </Canvas>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Rectangle 
                Fill="Red"
                Height="1"
                Width="{Binding NoteLength}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

The X and Y coordinates of each Rectangle are set by our ItemContainerStyle. We can't do this in the DataTemplate because each Rectangle will not be a direct child element of the ItemsPanel Canvas. Our ItemContainerStyle is very basic, simply holding a ContentPresenter. But it is in here that we can position our rectangles by binding to AbsoluteTime and NoteNumber of the NoteOn event and using them to set the Canvas.Left and Canvas.Top values. Here's the style:

<Style x:Key="PianoRollItemContainerStyle" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Canvas.Left" Value="{Binding AbsoluteTime}" />
    <Setter Property="Canvas.Top" Value="{Binding NoteNumber}" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <ContentPresenter x:Name="ContentHost" 
                                  Margin="{TemplateBinding Padding}" 
                    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

So now we have a functional (albeit still very basic) Piano Roll view with only one line of code behind.

WPF Binding to Canvas

As cool as this is, there are a number of issues with the data binding approach.

1. Speed - it is not nearly as responsive as the old control. I could look into "virtualising" the control, but I'm not sure I want to delve into this at the moment.

2. We will need binding converters to set the Width of the canvas and the real note position(currently it is upside-down with low note numbers at the top) if we want to do this right. This means writing more code, and we start to lose the elegance of doing it all in XAML.

3. Using a ListBox means that there is mouse and keyboard handling built-in for us, but it probably does not do what we want. I found it became very unresponsive when I clicked around, and occasionally some of the notes appeared to be highlighted, but not the ones I expected. There may be a lower-level ItemsControl we could use instead of ListBox that could help us.

Conclusion

We want our final PianoRoll control to be easy to use with data binding, but I am not sure yet that this is the best approach internally for implementing a complex control like this. I'll probably progress without the Data Binding for now, but with a view to using Data Binding wherever it does make sense. As usual I welcome any comments and suggestions, as I am still very much a beginner when it comes to WPF.

Post a Comment