3
votes

Scenario

I have a custom combo box where i have a label in the Combobox selection box. I need to change the label as I noted in the second image. But I want to do it only when I select items by selecting the check box. I can select multiple items, so the label should be updated as a comma separated value of selected items. If there is not enough space to display the full text of the label there should be "..." symbol to indicate that there are more items selected in the combo box.

enter image description here

I created a custom Label by inheriting the text Box control where I do all the changes in the callback event of a Dependency property. (Check custom Text Box code)

Now the problem is that the callback event in the custom Text box control is not firing when I change the bounded property in the View model (I am doing this by adding values to the observable collection in the code behind in check box on Check event. Please Check check box event code).

I can see that first time when I load default data in the view model the line is hit by the break point in the "Getter" part of "SelectedFilterResources" . But I never get a hit in the Setter part of the property.

Custom Text Box

The custom text box has the "CaptionCollectionChanged" callback event. It is the place where I have all logic to achieve my scenario. "Resources item" here is a type of Model.

    public class ResourceSelectionBoxLable : TextBox
    {
        public override void OnApplyTemplate()
        {
         base.OnApplyTemplate();
        IsReadOnly = true;
        }


        public static List<ResourceItem> LocalFilterdResources = new List<ResourceItem>();

        #region Dependancy Properties

        public static readonly DependencyProperty FilterdResourcesProperty =
            DependencyProperty.Register("SelectedFilterdResources",
                typeof (ObservableCollection<ResourceItem>),
                typeof (ResourceSelectionBoxLable),
                new PropertyMetadata(new ObservableCollection<ResourceItem>(),
                    CaptionCollectionChanged));

        public ObservableCollection<ResourceItem> SelectedFilterdResources
        {
            get
            {
                return
                (ObservableCollection<ResourceItem>) GetValue(FilterdResourcesProperty);
            }
            set
            {
                SetValue(FilterdResourcesProperty, value);
                LocalFilterdResources = new List<ResourceItem>(SelectedFilterdResources);
            }
        }

        #endregion

        private static void CaptionCollectionChanged(DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            var resourceSelectionBoxLable = d as ResourceSelectionBoxLable;
            if (resourceSelectionBoxLable != null)
            {
                if (LocalFilterdResources.Count <= 0)
                {
                    resourceSelectionBoxLable.Text = "Resources"
                }
                else
                {
                    var actualwidthOflable = resourceSelectionBoxLable.ActualWidth;
                    var newValue = e.NewValue as string;

                    //Get the Wdith of the Text in Lable
                    TextBlock txtMeasure = new TextBlock();
                    txtMeasure.FontSize = resourceSelectionBoxLable.FontSize;
                    txtMeasure.Text = newValue;
                    double textwidth = txtMeasure.ActualWidth;

                    //True if Text reach the Limit
                    if (textwidth > actualwidthOflable)
                    {
                        var appendedString = string.Join(", ",
                            LocalFilterdResources.Select(item => item.ResourceCaption)
                                .ToArray());
                        resourceSelectionBoxLable.Text = appendedString;
                    }
                    else
                    {
                        if (LocalFilterdResources != null)
                        {
                            var morestring = string.Join(", ",
                                (LocalFilterdResources as IEnumerable<ResourceItem>).Select(item => item.ResourceCaption)
                                    .ToArray());

                            var subsring = morestring.Substring(0, Convert.ToInt32(actualwidthOflable) - 4);
                            resourceSelectionBoxLable.Text = subsring + "...";
                        }
                    }
                }
            }
        }
    }

Custom Combo Box.

This is the control where I use the above custom label. This is also a custom control so most of the properties and styles in this controls are custom made. "DPItemSlectionBoxTemplate" is a dependency property where I enable the Selection Box of the combo box by adding an attached property to the control template. This control works fine, because I use this control in other places in my system for different purposes.

                    <styles:CommonMultiComboBox 
                                x:Name="Resourcescmb" IsEnabled="{Binding IsResourceComboEnable,Mode=TwoWay}" 
                                IsTabStop="False" 
                                >

                        <styles:CommonMultiComboBox.ItemDataTemplate>
                            <DataTemplate>
                                <CheckBox   IsChecked="{Binding IsSelect, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Click="CheckBox_Click" 
                                            Content="{Binding ResourceCaption}"
                                            Style="{StaticResource CommonCheckBoxStyle}"
                                            Tag ="{Binding}"
                                            Checked="Resource_ToggleButton_OnChecked" />
                            </DataTemplate>
                        </styles:CommonMultiComboBox.ItemDataTemplate>

                        <styles:CommonMultiComboBox.DPItemSlectionBoxTemplate>
                            <DataTemplate>
                                <filtersTemplate:ResourceSelectionBoxLable 
                                    Padding="0"
                                    Height="15"
                                    FontSize="10"
                                    SelectedFilterdResources="{Binding DataContext.FilterdResources,ElementName=root ,Mode=TwoWay}" />

                            </DataTemplate>
                        </styles:CommonMultiComboBox.DPItemSlectionBoxTemplate>
                    </styles:CommonMultiComboBox>

ViewModel

private ObservableCollection<ResourceItem> _resourceItems;
        public ObservableCollection<ResourceItem> FilterdResources
        {
            get { return _resourceItems; }
            set
            {
                SetOnChanged(value, ref _resourceItems, "FilterdResources");
            }
        }

Constructor of View Model

FilterdResources=new ObservableCollection<ResourceItem>();

"SetOnChanged" is a method in the View Model base class where we have the INotifyPropertichanged implementation.

Check Box Event

private void Resource_ToggleButton_OnChecked(object sender, RoutedEventArgs e)
        {

            var  senderControl = sender as CheckBox;
            if(senderControl==null)
                return;

            var selectedContent=senderControl.Tag as ResourceItem;

            if (selectedContent != null)
            {
                    ViewModel.FilterdResources.Add(selectedContent);

            }
}

I can access the View Model from the Code behind through the View Model Property.

Why is the call back event not notified when I change bounded values? Am i missing something here? Dependency properties are supposed to work for two way bindings aren't they? Could any one please help me on this?

Thanks in advance.

1
I noticed some issues that might bite you someday, thought I let you know: 1. the way you set up the dafault value for "SelectedFilterdResources"[sic] dependency property is creating an accidental singleton. 2. I would stick to the convention of naming the dependency property exactly like the static DP backingfield. 3. don't put code in the setter of a DP, it will not be called by the binding engine, use a propertyChangedCallback instead. - Martin
@Martin.Thanks. Yap I moved code from the Setter to call back event.Can you explain little bit more about how it SelectedFilterdResources accidentally creates Singleton ? - Thabo
The static backingfield allows only for static stuff (aka the same for all instances) to be associated with it. Therefore the collection instance you created as a default is shared among all instances, hence a singleton. Use the constructor instead, you can assign a freshly created collection instance as a default value for the DP there. - Martin
@Martin.Thanks. I moved it inside constructor :) - Thabo

1 Answers

1
votes

Looks like your issue is that you're expecting the CaptionCollectionChanged event to fire when the bound collection is changed (i.e. items added or removed). When in fact this event will fire only when you're changing an instance of the bound object.

What you need to do here is to subscribe to ObservableCollection's CollectionChanged event in the setter or change callback (which you already have - CaptionCollectionChanged) of your dependency property.

public static readonly DependencyProperty FilterdResourcesProperty =
        DependencyProperty.Register("SelectedFilterdResources",
            typeof (ObservableCollection<ResourceItem>),
            typeof (ResourceSelectionBoxLable),
            new PropertyMetadata(new ObservableCollection<ResourceItem>(),
                CaptionCollectionChanged));


private static void CaptionCollectionChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs args)
    {
        var collection = args.NewValue as INotifyCollectionChanged;
        if (collection != null)
        {
            var sender = d as ResourceSelectionBoxLable;
            if (sender != null)
            {
                collection.CollectionChanged += sender.BoundItems_CollectionChanged;
            }                
        }
    }

    private void BoundItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // Do your control logic here.
    }

Don't forget to add cleanup logic - unsubscribe from collection change when collection instance is changed and so on.