lecloneur
lecloneur

Reputation: 424

UserControl binding to observablecollection not working

I looked around but still can't find a solution...

I made a UserControl which is basically a slider but with few custom features.

public partial class CustomSlider : UserControl
{
    public CustomSlider()
    {
        InitializeComponent();
        this.DataContext = this;

        CMiXSlider.ApplyTemplate();
        Thumb thumb0 = (CMiXSlider.Template.FindName("PART_Track", CMiXSlider) as Track).Thumb;
        thumb0.MouseEnter += new MouseEventHandler(thumb_MouseEnter);
    }

    private void thumb_MouseEnter(object sender, MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed && e.MouseDevice.Captured == null)
        {
            MouseButtonEventArgs args = new MouseButtonEventArgs(e.MouseDevice, e.Timestamp, MouseButton.Left);
            args.RoutedEvent = MouseLeftButtonDownEvent;
            (sender as Thumb).RaiseEvent(args);
        }
    }

    public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register("Value", typeof(double), typeof(CustomSlider), new PropertyMetadata(0.0));
    public double Value
    {
        get { return (double)this.GetValue(ValueProperty); }
        set { this.SetValue(ValueProperty, value); }
    }
}

The XAML :

<UserControl x:Class="CMiX.CustomSlider"
         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:local="clr-namespace:CMiX"
         mc:Ignorable="d" 
         d:DesignHeight="139.8" d:DesignWidth="546.2">
<UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="/CMiX_UserControl;component/RessourceDictionnaries/Brushes/GenericBrushes.xaml"/>
            <ResourceDictionary Source="/CMiX_UserControl;component/RessourceDictionnaries/Styles/BaseSliderStyle.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</UserControl.Resources>
<Grid>
    <Slider x:Name="CMiXSlider" Style="{StaticResource BaseSliderStyle}" 
            Value="{Binding Value, Mode=TwoWay, RelativeSource={RelativeSource AncestorType={x:Type local:CustomSlider}}}" 
            IsMoveToPointEnabled="True" Minimum="0.0" Maximum="1.0"/>
</Grid>

Then I use it inside another UserControl as so :

<CMiX:CustomSlider x:Name="SliderTest" Grid.Row="2" Value="{Binding ChannelsAlpha[0], Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

And I'm trying to bind to this ObservableCollection (the same slider will be used 6 times) :

private ObservableCollection<double> _ChannelsAlpha = new ObservableCollection<double>(new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 });
    public ObservableCollection<double> ChannelsAlpha
    {
        get { return _ChannelsAlpha; }
        set { _ChannelsAlpha = value; }
    }

Problem is, binding is not happening in any way. And what I particularly don't get is if I use this standard slider :

<Slider x:Name="Ch0_Alpha" Margin="1" IsMoveToPointEnabled="True" Minimum="0.0" Maximum="1.0" Orientation="Horizontal" Value="{Binding DataContext.ChannelsAlpha[0], ElementName=Ch0_Alpha, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

Then it's working as expected.

Upvotes: 2

Views: 1033

Answers (1)

You can't bind to anything because you very carefully broke your DataContext:

this.DataContext = this;

Don't do that. To bind to properties of the UserControl itself, use RelativeSource bindings, like... exactly like the one you already used:

<Slider 
    x:Name="CMiXSlider" 
    Style="{StaticResource BaseSliderStyle}" 
    Value="{Binding Value, Mode=TwoWay, RelativeSource={RelativeSource AncestorType={x:Type local:CustomSlider}}}" 
    IsMoveToPointEnabled="True" 
    Minimum="0.0" 
    Maximum="1.0"
    />

I don't see anywhere you're using the DataContext inside the UserControl XAML, so I'd just delete that line in the constructor and go have a beer.

Well, first, for the sake of good form, I'd get rid of the template stuff in the constructor, and move it to OnApplyTemplate:

public CustomSlider()
{
    InitializeComponent();
}

public override void OnApplyTemplate()
{
    Thumb thumb0 = (CMiXSlider.Template.FindName("PART_Track", CMiXSlider) as Track).Thumb;
    thumb0.MouseEnter += new MouseEventHandler(thumb_MouseEnter);
}

Then beer.

P.S. Here's how to debug bindings:

Value="{Binding ChannelsAlpha[0], PresentationTraceSources.TraceMode=High, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"

Add PresentationTraceSources.TraceMode=High, then watch the Output window in VS at runtime. It'll tell you all the steps it takes trying to resolve the binding. What it'll tell you in this case is that it's looking for ChannelIsAlpha on that instance of CMiX:CustomSlider instead of on the viewmodel. That's your clue that the problem was created by getting rid of the inherited value of DataContext. In your constructor, this.DataContext was the parent view's viewmodel until you set it to this (pop in a breakpoint and see).

This is why some of us cranky old folks have such a burr about not setting this.DataContext = this;. First you do it in cases where it's relatively harmless, then you start thinking that's just necessary boilerplate, and here you are. In fact, it's never necessary.

Bindings and DataContext are painful to get used to because there's this implicit thing going on. I found it very weird at first. Just remember that the assumption is that by default, all bindings will want to bind to the viewmodel, and you should always be able to assume that wherever you are, DataContext will be the viewmodel. And never set DataContext explicitly.

Binding to anything but the viewmodel is a special case: RelativeSource={RelativeSource AncestorType=FooBar}, Source={StaticResource WhateverKey}, ElementName=FooBarListBox, RelativeSource={RelativeSource Self}, or whatever.

Upvotes: 3

Related Questions