-2

I am trying to use the WPF Grid as an ItemsControl using attached properties for the purposes of creating a scalable Piano Keyboard. Each key in the keyboard may span 1 to three columns depending upon what precedes and succeeds it and will span 1 row if sharp or 2 if natural. I already have 2 attached properties for setting the Grid's Column Count and Row Count dynamically (albeit these will need to be adjusted to support the setting of each column/row's width/height).

What I now need to implement are two attachable properties for the ItemsSource (Keys) and the ItemTemplate (PianoKeyView). I need to use this on the Grid control because ItemsControl only supports UniformGrid as a Grid for its ItemsPanel and also doesn't assignment of specific items to specific columns/rows. My Piano Keyboard would require 17 columns per octave of keys but an ItemsControl would only create 12 columns in a UniformGrid as there would only be 12 keys passed to it. I have included an image of a 1-octave Piano Keyboard with the index of each required column included.

PianoKeyboard Grid Column Indices

This is my code for the keyboard as it currently stands, I am missing the implementation for GridExtensions.ItemsSource and GridExtensions.ItemTemplate. GridExtensions is a static class containing attachable properties.

<UserControl x:Class="SphynxAlluro.Music.Wpf.PianoKeyboard.View.PianoKeyboardView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
         xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
         xmlns:converters="http://schemas.sphynxalluro.com/converters"
         xmlns:local="clr-namespace:SphynxAlluro.Music.Wpf.PianoKeyboard.View"
         xmlns:prism="http://www.codeplex.com/prism"
         xmlns:sphynxAlluroControls="http://schemas.sphynxalluro.com/controls"
         xmlns:wpfBindingExtensions="http://schemas.sphynxalluro.com/bindingExtensions"
         mc:Ignorable="d"
         d:DesignHeight="200" d:DesignWidth="600">
<UserControl.Resources>
    <converters:KeysToColumnsCountConverter x:Key="keysToColumnsCountConverter"/>
    <converters:KeysToRowsCountConverter x:Key="keysToRowsCountConverter"/>
    <converters:IsSharpToRowSpanConverter x:Key="isSharpToRowSpanConverter"/>
    <converters:KeysCollectionAndKeyToColumnIndexConverter x:Key="keysCollectionAndKeyToColumnIndexConverter"/>
    <converters:KeysCollectionAndKeyToColumnSpanConverter x:Key="keysCollectionAndKeyToColumnSpanConverter"/>
</UserControl.Resources>
<Grid wpfBindingExtensions:GridExtensions.ItemsSource="{Binding Keys}"
      wpfBindingExtensions:GridExtensions.ItemsOrientation="Horizontal"
      wpfBindingExtensions:GridExtensions.ColumnCount="{Binding Keys, Converter={StaticResource keysToColumnsCountConverter}}"
      wpfBindingExtensions:GridExtensions.RowCount="{Binding Keys, Converter={StaticResource keysToRowsCountConverter}}">
    <wpfBindingExtensions:GridExtensions.ItemTemplate>
        <DataTemplate>
            <local:PianoKeyView Grid.RowSpan="{Binding Note.IsSharp, Mode=OneTime, Converter={StaticResource isSharpToRowSpanConverter}}"
                            DataContext="{Binding}">
                <Grid.Column>
                    <MultiBinding Converter="{StaticResource keysCollectionAndKeyToColumnIndexConverter}" Mode="OneTime">
                        <Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="Items"/>
                        <Binding/>
                    </MultiBinding>
                </Grid.Column>
                <Grid.ColumnSpan>
                    <MultiBinding Converter="{StaticResource keysCollectionAndKeyToColumnSpanConverter}" Mode="OneTime">
                        <Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="Items"/>
                        <Binding/>
                    </MultiBinding>
                </Grid.ColumnSpan>
            </local:PianoKeyView>
        </DataTemplate>
    </wpfBindingExtensions:GridExtensions.ItemTemplate>
</Grid>

And this is the code for the ItemTemplateChanged handler for the ItemTemplate attachable property in GridExtensions, note the two TODOs above the lines which do not compile.

private static void ItemTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var itemTemplate = (DataTemplate)e.NewValue;
    var itemsSource = GetItemsSource(d);
    var itemsSourceCount = itemsSource.Count();
    var itemsOrientation = GetItemsOrientation(d);
    var gridChildren = ((Grid)d).Children;

    gridChildren.Clear();

    switch (itemsOrientation)
    {
        case Orientation.Horizontal:
            foreach (var item in itemsSource)
            {
                var itemFactory = new FrameworkElementFactory(item.GetType());

                //TODO: Find out where the ContentProperty for Grid is.
                itemFactory.SetValue(d.ContentProperty, item);
                itemTemplate.VisualTree = itemFactory;

                //TODO: Find out how to add the applied itemTemplate.
                gridChildren.Add(itemTemplate);
            }
            break;
        case Orientation.Vertical:
            break;
        default:
            throw new EnumValueNotSupportedException(itemsOrientation, nameof(itemsOrientation).ToPascalCase());
    }
}
Sphynx
  • 135
  • 2
  • 12
  • 5 converters and 4 attached DP? I would say that instead of complicating view even more one should simplify their view model to have data in the form more convenient for bindings in ItemsControl – ASh Nov 03 '17 at 14:02
  • Another option is to use a horizontal StackPanel in your ItemsPanelTemplate, and play with negative margins and Panel.ZIndex via style/binding/whatever to make the non-adjacent white keys extend and meet "under" the black keys. – 15ee8f99-57ff-4f92-890c-b56153 Nov 03 '17 at 14:03
  • @Ed Plunkett @ASh I would like to use an ItemsControl but how do I generate 17 columns for 12 items? I did originally have this as an `ItemsControl` but the attachable `Grid.Column` and `Grid.Row` properties on `PianoKey` weren't working with the `UniformGrid` and as far as I remember `Grid` used as an `ItemsPanelTemplate` didn't generate any columns. – Sphynx Nov 03 '17 at 14:07
  • @Sphynx It's not 12 items, it's 17 items. A above middle C and A below middle C are not the same note. And it's not a UniformGrid because the keys are of different widths. Grid never generates its own columns; that's the programmer's job. – 15ee8f99-57ff-4f92-890c-b56153 Nov 03 '17 at 14:09
  • @Ed Plunkett There's only one A in that keyboard though (spanning columns 12 to 14), its 12 notes but 17 columns required to display the 12 notes as keys in a piano keyboard. The `ItemContainerGenerator` is something I've not come across before though so I'll have a look into that. – Sphynx Nov 03 '17 at 14:13
  • @Sphynx Oh right, I forgot you were using multiple columns per key, my bad, Anyway see answer, you don't need to do all that stuff. – 15ee8f99-57ff-4f92-890c-b56153 Nov 03 '17 at 14:35
  • Please provide a complete explanation of what you’re trying to do. You can’t expect people to take more that two shots at an unpredictably moving target. – 15ee8f99-57ff-4f92-890c-b56153 Nov 03 '17 at 15:48
  • I specified that I needed the piano keyboard to be scaleable, I also specified that that it had an ItemsSource of Keys which I thought would suggest that it could be any number of Keys (otherwise I would have just had a RootNote property and generated a constant length of keys from that). I was prepared to work with your answer and post my own but you removed it! – Sphynx Nov 03 '17 at 15:53

1 Answers1

0

What I was trying to achieve with the Grid directly could be achieved with an ItemsControl with an ItemsPanel of Grid.

It turns out the missing piece that was needed was a Style with a TargetType of ContentPresenter. In this style, the attachable Grid properties such as Grid.RowSpan, Grid.Column and Grid.ColumnSpan are settable via the appropriate converters which take in the ItemsControl and Key's DataContext and return the required integer. The Z-Index of the keys is also settable here so that the sharp keys appear above the natural keys.

<ItemsControl.ItemContainerStyle>
    <Style TargetType="ContentPresenter">
        <Setter Property="Grid.RowSpan" Value="{Binding Note.IsSharp, Mode=OneTime, Converter={StaticResource isSharpToRowSpanConverter}}"/>
        <Setter Property="Grid.Column">
            <Setter.Value>
                <MultiBinding Converter="{StaticResource keysCollectionAndKeyToColumnIndexConverter}" Mode="OneTime">
                    <Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="Items"/>
                    <Binding/>
                </MultiBinding>
            </Setter.Value>
        </Setter>
        <Setter Property="Grid.ColumnSpan">
            <Setter.Value>
                <MultiBinding Converter="{StaticResource keysCollectionAndKeyToColumnSpanConverter}" Mode="OneTime">
                    <Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="Items"/>
                    <Binding/>
                </MultiBinding>
            </Setter.Value>
        </Setter>
        <Setter Property="Panel.ZIndex" Value="{Binding Note.IsSharp, Converter={StaticResource booleanToIntegerConverter}}"/>
    </Style>
</ItemsControl.ItemContainerStyle>

This then greatly simplifies the ItemTemplate to the following:

<ItemsControl.ItemTemplate>
    <DataTemplate>
        <local:PianoKeyView DataContext="{Binding}"/>
    </DataTemplate>
</ItemsControl.ItemTemplate>

The ItemsControl.ItemsPanel is responsible for generating the Grid in which these PianoKeyViews wrapped in their ContentPresenters will then be placed into.

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <Grid wpfBindingExtensions:GridExtensions.ColumnDefinitions="{Binding Keys, Converter={StaticResource keysToColumnDefinitionsConverter}}"
              wpfBindingExtensions:GridExtensions.RowDefinitions="{Binding Keys, Converter={StaticResource keysToRowDefinitionsConverter}}"/>
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

I have modified my original ColumnCount and RowCount attachable properties to instead take an IEnumerable<ColumnDefinition>/IEnumerable<RowDefinition> as appropriate so that the size of each column/row can also be passed to the properties (which I do so in this case via a converter which takes in all PianoKeyViewModels and returns a ColumnDefinition/RowDefinition for each with appropriate star sizing. The assignment for RowDefinitions is only for an edge case under which only natural or sharp keys are needed in the keyboard (e.g. B to C, E to F or a single sharp or natural key). The code for the RowDefinitions attached property is essentially the same logic as with the ColumnDefinitions property (only working with RowDefinitions instead of ColumnDefinitions) so I'll just post the one for ColumnDefinitions here:

public static class GridExtensions
{
    // Using a DependencyProperty as the backing store for ColumnDefinitions.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ColumnDefinitionsProperty =
        DependencyProperty.RegisterAttached(
            nameof(ColumnDefinitionsProperty).Substring(0, nameof(ColumnDefinitionsProperty).Length - "Property".Length),
            typeof(IEnumerable<ColumnDefinition>),
            typeof(GridExtensions),
            new PropertyMetadata(Enumerable.Empty<ColumnDefinition>(), ColumnDefinitionsChanged));

    public static IEnumerable<ColumnDefinition> GetColumnDefinitions(DependencyObject obj)
        => (IEnumerable<ColumnDefinition>)obj.GetValue(ColumnDefinitionsProperty);

    public static void SetColumnDefinitions(DependencyObject obj, IEnumerable<ColumnDefinition> value)
        => obj.SetValue(ColumnDefinitionsProperty, value);

    private static void ColumnDefinitionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var columnDefinitionCollection = ((Grid)d).ColumnDefinitions;
        var newColumnDefinitions = (IEnumerable<ColumnDefinition>)e.NewValue;
        var columnCount = newColumnDefinitions.Count();

        columnDefinitionCollection.Clear();

        foreach (var newColumnDefinition in newColumnDefinitions)
            columnDefinitionCollection.Add(newColumnDefinition);
    }
}

For more on the attached properties, please see "Using a Grid as the Panel for an ItemsControl" (http://blog.scottlogic.com/2010/11/15/using-a-grid-as-the-panel-for-an-itemscontrol.html) which is where I found the original code from which I derived the above static class. Otherwise, the key elements here are:

  1. Assign attachable panel properties (such as Grid.Column and Panel.ZOrder) to the ContentPresenter style in ItemsControl.ItemContainerStyle.
  2. Set the ItemsControl.ItemsPanel to an ItemsPanelTemplate of Grid and set up the Grid's ColumnDefinitions and RowDefinitions from there.

I didn't post all my converters here as this answer is already getting quite long but someone let me know if they feel they are relevant to post. Otherwise, here was the end result...

PianoKeyboardView

Sphynx
  • 135
  • 2
  • 12