Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Binding ContentControl to an ObservableCollection if Count == 1

Tags:

binding

wpf

how can I bind the Content of a ContentControl to an ObservableCollection. The control should show an object as content only if the ObservableColelction contains exactly one object (the object to be shown).

Thanks, Walter

like image 534
Walter Avatar asked Jun 12 '10 07:06

Walter


2 Answers

This is easy. Just use this DataTemplate:

<DataTemplate x:Key="ShowItemIfExactlyOneItem">

  <ItemsControl x:Name="ic">
    <ItemsControl.ItemsPanel>
      <ItemsPanelTemplate><Grid/></ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
  </ItemsControl>

  <DataTemplate.Triggers>
    <DataTrigger Binding="{Binding Count}" Value="1">
      <Setter TargetName="ic" Property="ItemsSource" Value="{Binding}" />
    </DataTrigger>
  </DataTemplate.Triggers>

</DataTemplate>

This is used as the ContentTemplate of your ContentControl. For example:

<Button Content="{Binding observableCollection}"
        ContentTemplate="{StaticResource ShowItemIfExactlyOneItem}" />

That's all you need to do.

How it works: The template normally contains an ItemsControl with no items, which is invisible and has no size. But if the ObservableCollection that is set as Content ever has exactly one item in it (Count==1), the trigger fires and sets the ItemsSource of the ItmesControl, causing the single item to display using a Grid for a panel. The Grid template is required because the default panel (StackPanel) does not allow its content to expand to fill the available space.

Note: If you also want to specify a DataTemplate for the item itself rather than using the default template, set the "ItemTemplate" property of the ItemsControl.

like image 177
Ray Burns Avatar answered Sep 19 '22 23:09

Ray Burns


+1, Good question :)

You can bind the ContentControl to an ObservableCollection<T> and WPF is smart enough to know that you are only interested in rendering one item from the collection (the 'current' item)

(Aside: this is the basis of master-detail collections in WPF, bind an ItemsControl and a ContentControl to the same collection, and set the IsSynchronizedWithCurrentItem=True on the ItemsControl)

Your question, though, asks how to render the content only if the collection contains a single item... for this, we need to utilize the fact that ObservableCollection<T> contains a public Count property, and some judicious use of DataTriggers...

Try this...

First, here's my trivial Model object, 'Customer'

public class Customer
{
    public string Name { get; set; }
}

Now, a ViewModel that exposes a collection of these objects...

    public class ViewModel
    {
        public ViewModel()
        {
            MyCollection = new ObservableCollection<Customer>();

            // Add and remove items to check that the DataTrigger fires correctly...
            MyCollection.Add(new Customer { Name = "John Smith" });
            //MyCollection.Add(new Customer { Name = "Mary Smith" });
        }

        public ObservableCollection<Customer> MyCollection { get; private set; }
    }

Set the DataContext in the Window to be an instance of the VM...

    public Window1()
    {
        InitializeComponent();

        this.DataContext = new ViewModel();
    }

and here's the fun bit: the XAML to template a Customer object, and set a DataTrigger to remove the 'Invalid Count' part if (and only if) the Count is equal to 1.

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1"
    Title="Window1" Height="300" Width="300">

    <Window.Resources>
        <Style TargetType="{x:Type ContentControl}">
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate x:Name="template">
                        <Grid>
                            <Grid Background="AliceBlue">
                                <TextBlock Text="{Binding Name}" />
                            </Grid>
                            <Grid x:Name="invalidCountGrid" Background="LightGray" Visibility="Visible">
                                <TextBlock 
                                    VerticalAlignment="Center" HorizontalAlignment="Center"
                                    Text="Invalid Count" />
                            </Grid>
                        </Grid>
                        <DataTemplate.Triggers>
                            <DataTrigger Binding="{Binding Count}" Value="1">
                                <Setter TargetName="invalidCountGrid" Property="Visibility" Value="Collapsed" />
                            </DataTrigger>
                        </DataTemplate.Triggers>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <ContentControl
        Margin="30"
        Content="{Binding MyCollection}" />

</Window>

UPDATE

To get this dynamic behaviour working, there is another class that will help us... the CollectionViewSource

Update your VM to expose an ICollectionView, like:

public class ViewModel
{
    public ViewModel()
    {
        MyCollection = new ObservableCollection<Customer>();
        CollectionView = CollectionViewSource.GetDefaultView(MyCollection);
    }
    public ObservableCollection<Customer> MyCollection { get; private set; }
    public ICollectionView CollectionView { get; private set; }

    internal void Add(Customer customer)
    {
        MyCollection.Add(customer);
        CollectionView.MoveCurrentTo(customer);
    }
}

And in the Window wire a button Click event up to the new 'Add' method (You can use Commanding if you prefer, this is just as effective for now)

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        _viewModel.Add(new Customer { Name = "John Smith" });
    }

Then in the XAML, without changing the Resource at all - make this the body of your Window:

<StackPanel>
    <TextBlock Height="20">
        <TextBlock.Text>
            <MultiBinding StringFormat="{}Count: {0}">
                <Binding Path="MyCollection.Count" />
            </MultiBinding>
        </TextBlock.Text>
    </TextBlock>
    <Button Click="Button_Click" Width="80">Add</Button>
    <ContentControl
        Margin="30" Height="120"
        Content="{Binding CollectionView}" />
</StackPanel>

So now, the Content of your ContentControl is the ICollectionView, and you can tell WPF what the current item is, using the MoveCurrentTo() method. Note that, even though ICollectionView does not itself contain properties called 'Count' or 'Name', the platform is smart enough to use the underlying data source from the CollectionView in our Bindings...

like image 29
kiwipom Avatar answered Sep 17 '22 23:09

kiwipom