Monday 12 December 2011

Silverlight (WP7) ListBox & ItemsControl Item Commanding

ListBoxes are a really powerful and flexible way of displaying data in Silverlight, the main problem I find is when you want to use an MVVM pattern and detect when an item is selected and re-selected. It is possible to bind to the SelectedItem property and look for changes in the VM to perform some kind of action, however if you want to select the same item again you need to select something else, then re-select it because SelectedItem becomes latched.

Instead of using a ListBox, it is more flexible to us an ItemsControl (which a ListBox derives from). This has the functionality we require but without the SelectedItem dependency property which we aren't using. The following example has an Items control with a button containing an image in the ItemTemplate:

<Grid x:Name="LayoutRoot">
  <ItemsControl ItemsSource="{Binding Path=Items, Mode=OneWay}">
    <ItemsControl.ItemsPanel>
      <ItemsPanelTemplate>
        <StackPanel Orientation="Horizontal" />
      </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <Button>
          <Image Source="{Binding Path=Url, Mode=OneWay}" Stretch="Uniform" />
        </Button>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</Grid>

This is fairly straight forward, but the problem is, if you add a command to the button like this:

<Button Command="{Binding ImageCommand}" CommandParameter="{Binding}" >
  <Image Source="{Binding Path=Url, Mode=OneWay}" Stretch="Uniform" />
</Button>

The button will try and bind to a command in the data item which doesn't exist; instead of the parent view model.

To get round this, the command needs to get outside of the bound item's context, into the main view model by binding to the grid's data context (it could be any named, non-templated element on a page/user control) using the ElementName and Path binding properties:

<Button Command="{Binding ElementName=LayoutRoot, Path=DataContext.ImageCommand}"
  CommandParameter="{Binding}" >
  <Image Source="{Binding Path=Url, Mode=OneWay}" Stretch="Uniform" />
</Button>

This can now easily be bound to a view model containing a command like this:

private ICommand _imageCommand = null;

public ICommand ImageCommand

{
    get { return this._imageCommand; }
}

private bool IsCommandExecutable

{
    get { return true; }
}

// Call this in VM constructor

private void InitialiseCommands()
{
    // Join
    this._imageCommand = new DelegateCommand((param) =>
    {
        this.DoSomething((ImageDetails)param);
        },
            (p) =>
            {
                return this.IsCommandExecutable;
            });
    }

private void DoSomething(ImageDetails obj)

{

}


This technique obviously works with ListBox controls as well, so buttons can be placed alongside other content whilst maintaining the selection mechanism. This example has a delete button alongside some title text:

<Grid x:Name="LayoutRoot">
  <ListBox ItemsSource="{Binding Items, Mode=OneWay}"
    SelectedItem="{Binding SelectedItem, Mode=TwoWay>
    <ListBox.ItemsPanel>
      <ItemsPanelTemplate>
        <StackPanel Orientation="Horizontal" />
      </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
      <DataTemplate>
<Grid>
 <Grid.ColumnDefinitions>
   <ColumnDefinition Width="Auto"/>
   <ColumnDefinition Width="*"/>
 <Grid.ColumnDefinitions>
          <TextBlock Text="{Binding Title} />
          <Button Grid.Column="1" Command="{Binding DeleteCommand}" />
        <Grid>
      </DataTemplate>
    </ListBox.ItemTemplate>
  </ListBox>
</Grid>

No comments:

Post a Comment