Search code examples
c#wpfwpf-controls

C# WPF two way binding with a user control not working


I created a user control and I want to reflect the updates made on the control to the ViewModel, but I can't get it to work.

The properties I want to bind TwoWay are SearchFilter and SelectedMembers. Edit : I managed to make SearchFilter work two ways. The problem is only with my ObservableCollection. Thus I will not mention it afterwards.

My component is defined as follow :

AutoCompComboBox.xaml.cs

    public partial class AutoCompComboBox : UserControl
    {
        
        #region SearchFilter
        public string SearchFilter
        {
            get => (string)GetValue(SearchFilterProperty);
            set => SetValue(SearchFilterProperty, value);
        }

        public static readonly DependencyProperty SearchFilterProperty =
            DependencyProperty.Register(nameof(SearchFilter), typeof(string), typeof(AutoCompComboBox),
                new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSearchFilterChangedCallBack));

        private static void OnSearchFilterChangedCallBack(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (sender is AutoCompComboBox autoComp)
            {
                autoComp.OnSearchFilterChanged();
            }
        }

        #endregion

        #region ItemsSource
        public object ItemsSource
        {
            get => GetValue(ItemsSourceProperty);
            set => SetValue(ItemsSourceProperty, value);
        }

        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register(nameof(ItemsSource), typeof(object), typeof(AutoCompComboBox), new PropertyMetadata(0));
        #endregion

        #region TextPath
        public string TextPath
        {
            get => (string)GetValue(TextPathProperty);
            set => SetValue(TextPathProperty, value);
        }

        public static readonly DependencyProperty TextPathProperty =
            DependencyProperty.Register(nameof(TextPath), typeof(string), typeof(AutoCompComboBox), new PropertyMetadata(""));
        #endregion

        #region DisplayMember
        public string DisplayMember
        {
            get => (string)GetValue(DisplayMemberProperty);
            set => SetValue(DisplayMemberProperty, value);
        }

        public static readonly DependencyProperty DisplayMemberProperty =
            DependencyProperty.Register(nameof(DisplayMember), typeof(string), typeof(AutoCompComboBox), new PropertyMetadata(""));
        #endregion

        #region SelectedMembers
        public ObservableCollection<object> SelectedMembers
        {
            get => (ObservableCollection<object>)GetValue(SelectedMembersProperty);
            set => SetValue(SelectedMembersProperty, value);
        }

        public static readonly DependencyProperty SelectedMembersProperty =
            DependencyProperty.Register(nameof(SelectedMembers), typeof(ObservableCollection<object>), typeof(AutoCompComboBox),
                new FrameworkPropertyMetadata(new ObservableCollection<object>(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
        #endregion

        private ObservableCollection<object> ObjCollection;
        private List<object> LstObject;
        public AutoCompComboBox()
        {
            InitializeComponent();
        }

        protected virtual void OnSearchFilterChanged()
        {
            if(LstObject != null)
            {
                ObjCollection = new ObservableCollection<object>();
                foreach (object obj in LstObject)
                {
                    string number = obj.GetType().GetProperty(TextPath).GetValue(obj, null).ToString().ToLower();
                    if (number.Contains(SearchFilter.ToLower()))
                    {
                        ObjCollection.Add(obj);
                    }
                }
                ItemsSource = ObjCollection;
            }
        }

        private void ComboBoxItem_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ClickCount == 1 && e.ChangedButton == MouseButton.Left)
            {
                ContentPresenter contentPresenter = sender as ContentPresenter;
                if (!SelectedMembers.Contains(contentPresenter.DataContext))
                {
                    SelectedMembers.Add(contentPresenter.DataContext);
                }
            }
        }

        private void ToggleButton_Checked(object sender, RoutedEventArgs e)
        {
            LstObject = (ItemsSource as IEnumerable<object>).Cast<object>().ToList();
            if (SearchFilter == null)
            {
                SearchFilter = "";
            }
            OnSearchFilterChanged();
        }

        private void ToggleButton_Unchecked(object sender, RoutedEventArgs e)
        {
            ItemsSource = new ObservableCollection<object>(LstObject);
        }

        private void CloseButton_Click(object sender, RoutedEventArgs e)
        {
            Button button = sender as Button;
            object value = button.DataContext;
            SelectedMembers.Remove(value);
        }
    }

AutoCompComboBox.xaml

<UserControl x:Class="Project.Components.GenericComponents.AutoCompComboBox"
             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:Project.Components.GenericComponents"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800"
             x:Name="AutoCompleteControl">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>

        <ListBox ItemsSource="{Binding SelectedMembers, ElementName=AutoCompleteControl, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
                 DisplayMemberPath="{Binding DisplayMember, ElementName=AutoCompleteControl, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
                 Height="50"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled">
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>

            <!--#region ListBox item style-->
            <ItemsControl.ItemContainerStyle>
                <Style>
                    <Setter Property="Control.Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                                <Border Background="#D5E2FB"
                                        BorderThickness="1"
                                        BorderBrush="#D5E2FB"
                                        CornerRadius="10"
                                        Margin="1"
                                        Height="22"
                                        HorizontalAlignment="Center"
                                        VerticalAlignment="Center">
                                    <DockPanel HorizontalAlignment="Center"
                                               VerticalAlignment="Center">
                                        <ContentPresenter HorizontalAlignment="Center"
                                                          VerticalAlignment="Center"
                                                          Margin="8,0,5,0"/>
                                        <!--#region Close item button style-->
                                        <Button Content="🗙"
                                                Click="CloseButton_Click">
                                            <Button.Style>
                                                <Style TargetType="Button">
                                                    <Setter Property="OverridesDefaultStyle" Value="True"/>
                                                    <Setter Property="Height" Value="20" />
                                                    <Setter Property="Width" Value="20" />
                                                    <Setter Property="Background" Value="Transparent" />
                                                    <Setter Property="Foreground" Value="#2B2B2B" />
                                                    <Setter Property="FontWeight" Value="8"/>
                                                    <Setter Property="Content" Value="🗙" />
                                                    <Setter Property="HorizontalContentAlignment" Value="Center" />
                                                    <Setter Property="VerticalContentAlignment" Value="Center" />
                                                    <Setter Property="BorderThickness" Value="1" />
                                                    <Setter Property="BorderBrush" Value="Transparent"/>
                                                    <Setter Property="Template">
                                                        <Setter.Value>
                                                            <ControlTemplate TargetType="Button">
                                                                <Grid Background="{TemplateBinding Background}">
                                                                    <Ellipse x:Name="ButtonBackground"
                                                                             Width="20"
                                                                             Height="20"/>
                                                                    <ContentPresenter HorizontalAlignment="Center"
                                                                                      VerticalAlignment="Center"
                                                                                      SnapsToDevicePixels="True">
                                                                    </ContentPresenter>
                                                                </Grid>
                                                                <ControlTemplate.Triggers>
                                                                    <Trigger Property="IsMouseOver" Value="True">
                                                                        <Setter TargetName="ButtonBackground" Property="Fill" Value="#F2F5F8" />
                                                                        <Setter Property="Cursor" Value="Hand" />
                                                                    </Trigger>
                                                                </ControlTemplate.Triggers>
                                                            </ControlTemplate>
                                                        </Setter.Value>
                                                    </Setter>
                                                </Style>
                                            </Button.Style>
                                        </Button>
                                        <!--#endregion-->
                                    </DockPanel>
                                </Border>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ItemsControl.ItemContainerStyle>
            <!--#endregion-->
        </ListBox>

        <ComboBox x:Name="AutoCompCB"
                  Grid.Row="1"
                  IsEditable="True"
                  Tag="False"
                  ItemsSource="{Binding ItemsSource, ElementName=AutoCompleteControl, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
                  DisplayMemberPath="{Binding DisplayMember, ElementName=AutoCompleteControl, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
            <ComboBox.Resources>
                <Style TargetType="{x:Type ComboBox}">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type ComboBox}">
                                <Grid x:Name="MainGrid"
                                      SnapsToDevicePixels="true">
                                    <Popup x:Name="PART_Popup"
                                           StaysOpen="True"
                                           AllowsTransparency="true"
                                           IsOpen="{Binding Tag, RelativeSource={RelativeSource TemplatedParent}}"
                                           Margin="1"
                                           PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}"
                                           Placement="Bottom"
                                           Width="{Binding Path=ActualWidth, ElementName=SearchField}">
                                        <Border x:Name="DropDownBorder"
                                                BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}"
                                                BorderThickness="1"
                                                Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}">
                                            <ScrollViewer x:Name="DropDownScrollViewer">
                                                <Grid RenderOptions.ClearTypeHint="Enabled">
                                                    <Canvas HorizontalAlignment="Left"
                                                            Height="0"
                                                            VerticalAlignment="Top"
                                                            Width="0">
                                                        <Rectangle x:Name="OpaqueRect"
                                                                   Height="{Binding ActualHeight, ElementName=DropDownBorder}"
                                                                   Width="{Binding ActualWidth, ElementName=DropDownBorder}"/>
                                                    </Canvas>
                                                    <ItemsPresenter x:Name="ItemsPresenter"
                                                                    KeyboardNavigation.DirectionalNavigation="Contained"
                                                                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                                                </Grid>
                                            </ScrollViewer>
                                        </Border>
                                    </Popup>
                                    <Border x:Name="SearchField">
                                        <Grid>
                                            <Grid.ColumnDefinitions>
                                                <ColumnDefinition Width="*"/>
                                                <ColumnDefinition Width="auto"/>
                                            </Grid.ColumnDefinitions>
                                            <!--<TextBox Text="{Binding SearchFilter, ElementName=AutoCompleteControl, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>-->
                                            <TextBox Text="{Binding SearchFilter, ElementName=AutoCompleteControl, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
                                            <!--#region Style du bouton pour ouvrir/fermer la popup -->
                                            <ToggleButton Width="25"
                                                          HorizontalAlignment="Right"
                                                          BorderBrush="{TemplateBinding BorderBrush}"
                                                          Background="{TemplateBinding Background}"
                                                          IsChecked="{Binding Tag, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
                                                          Checked="ToggleButton_Checked"
                                                          Unchecked="ToggleButton_Unchecked">
                                                <ToggleButton.Style>
                                                    <Style TargetType="{x:Type ToggleButton}">
                                                        <Setter Property="Template">
                                                            <Setter.Value>
                                                                <ControlTemplate TargetType="{x:Type ToggleButton}">
                                                                    <ContentPresenter />
                                                                </ControlTemplate>
                                                            </Setter.Value>
                                                        </Setter>
                                                        <Setter Property="Content">
                                                            <Setter.Value>
                                                                <Border Background="Transparent"
                                                                        BorderBrush="Transparent"
                                                                        BorderThickness="1">
                                                                    <Path Width="15"
                                                                          Height="15"
                                                                          Fill="#2B2B2B"
                                                                          Stretch="Uniform"
                                                                          Data="M903.232 256l56.768 50.432L512 768 64 306.432 120.768 256 512 659.072z" />
                                                                </Border>
                                                            </Setter.Value>
                                                        </Setter>
                                                        <Style.Triggers>
                                                            <Trigger Property="IsChecked" Value="True">
                                                                <Setter Property="Content">
                                                                    <Setter.Value>
                                                                        <Border Background="Transparent"
                                                                                BorderBrush="Transparent">
                                                                            <Path Width="15"
                                                                                  Height="15"
                                                                                  Fill="#2B2B2B"
                                                                                  Stretch="Uniform"
                                                                                  Data="M903.232 768l56.768-50.432L512 256l-448 461.568 56.768 50.432L512 364.928z"/>
                                                                        </Border>
                                                                    </Setter.Value>
                                                                </Setter>
                                                            </Trigger>
                                                        </Style.Triggers>
                                                    </Style>
                                                </ToggleButton.Style>
                                            </ToggleButton>
                                            <!-- #endregion -->
                                        </Grid>
                                    </Border>

                                    <ContentPresenter ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
                                                      ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
                                                      Content="{TemplateBinding SelectionBoxItem}"
                                                      ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}"
                                                      HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                                      IsHitTestVisible="false"
                                                      Margin="{TemplateBinding Padding}"
                                                      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                                </Grid>
                                <ControlTemplate.Triggers>
                                    <Trigger Property="HasItems" Value="false">
                                        <Setter Property="Height" TargetName="DropDownBorder" Value="95"/>
                                    </Trigger>
                                    <Trigger Property="IsEnabled" Value="false">
                                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                                        <Setter Property="Background" Value="#FFF4F4F4"/>
                                    </Trigger>
                                    <Trigger Property="IsGrouping" Value="true">
                                        <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                                    </Trigger>
                                    <Trigger Property="ScrollViewer.CanContentScroll" SourceName="DropDownScrollViewer" Value="false">
                                        <Setter Property="Canvas.Top" TargetName="OpaqueRect" Value="{Binding VerticalOffset, ElementName=DropDownScrollViewer}"/>
                                        <Setter Property="Canvas.Left" TargetName="OpaqueRect" Value="{Binding HorizontalOffset, ElementName=DropDownScrollViewer}"/>
                                    </Trigger>
                                </ControlTemplate.Triggers>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
                <Style TargetType="{x:Type ComboBoxItem}">
                    <Setter Property="SnapsToDevicePixels" Value="True" />
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="ComboBoxItem">
                                <ContentPresenter PreviewMouseDown="ComboBoxItem_PreviewMouseDown"/>

                                <!--<ControlTemplate.Triggers>
                                    <Trigger Property="IsMouseOver" Value="True" SourceName="ItemBorder">
                                        <Setter Property="Background" Value="#99ccff" TargetName="ItemBorder"/>
                                        <Setter Property="Opacity" Value="0.6"/>
                                    </Trigger>
                                </ControlTemplate.Triggers>-->
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ComboBox.Resources>
        </ComboBox>
    </Grid>
</UserControl>

The UserControl is consumed in my HomePage.xaml like this :

<GenericComps:AutoCompComboBox SearchFilter="{Binding UserFavorites.StringSearch}"
                                                       ItemsSource="{Binding UserFavorites.CollectionMembers}"
                                                       SelectedMembers="{Binding UserFavorites.CollectionSelectedMembers}"
                                                       TextPath="Numero"
                                                       DisplayMember="Numero"/>

In my HomePageVM.cs, I have defined :

public class HomePageVM : BaseVM
{
    public Utilisateur Utilisateur { get; set; }
    public Favoris UserFavorites { get; set; }

    public HomePageVM(Window ctxtWindow)
        {
            Utilisateur = BD.GetUser(Environment.UserName);
            UserFavorites = new Favoris(Utilisateur);
            UserFavorites.GetUserFavoris();
        }

And the class Favoris.cs contains :

public class Favoris : INotifyPropertyChanged
    {
        public Utilisateur Utilisateur { get; set; }
        public ObservableCollection<ColFavori> CollectionMembers { get; private set; }

        private string _stringSearch;
        public string StringSearch
        {
            get => _stringSearch;
            set
            {
                _stringSearch = value;
                OnPropertyChanged(nameof(StringSearch));
            }
        }

        public ObservableCollection<ColFavori> CollectionSelectedMembers { get; set; }

        public Favoris(Utilisateur utilisateur)
        {
            Utilisateur = utilisateur;
            StringSearch = "";
            CollectionSelectedMembers = new ObservableCollection<ColFavori>();
        }

        public GetUserFavoris()
        {
            DataTable ColTable = BD.GetColFav(Utilisateur);
            CollectionMembers = new ObservableCollection<ColFavori>(CommonMethods.ConvertToList<ColFavori>(ColTable));
            //CommonMethods.ConvertToList creates a List given the DataTable
        }

And the class ColFavori is defined like this.

    public class Colfavori
    {
        public long ID { get; set; }
        public string Numero { get; set; }
        public string Designation { get; set; }
    }

I don't have any problem getting my data to the user control as displaying data contained within CollectionMembers. When I add an object (ColFavori here for reference) by clicking it from the ComboBox, it is added into SelectedMembers and visible in the ListBox. But the item I binded SelectedMembers from (CollectionSelectedMembers) is not updated.

I tried following various answers from StackOverflow, including using FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, tweaking with Binding UpdateSourceTrigger and Binding Mode but couldn't get anything to work.

Edit : I tried changing ObservableCollection<object> to ObservableCollection<ColFavori> and the two way binding works. I found this while looking into the Xaml failed binding tab on Visual Studio and see that I had a conversion error.

So I need to find ho to make the collection generic, but the two way binding is working.


Solution

  • As mentionned at the bottom of my post the problem was not the binding, but an error while trying to convert types. The conversion from ObservableCollection<PNFavori> to ObservableCollection<object> did not work and caused a binding error.

    To make my list Generic and not break the binding still work, I used IList. It allowed me to keep doing operations onto the list (like add, remove, iterate) and have a working generic binding.

    Here is the code that changed :

    public IList SelectedMembers
    {
        get => (IList)GetValue(SelectedMembersProperty);
        set => SetValue(SelectedMembersProperty, value);
    }
    
    public static readonly DependencyProperty SelectedMembersProperty =
        DependencyProperty.Register(nameof(SelectedMembers), typeof(IList), typeof(AutoCompComboBox),
            new FrameworkPropertyMetadata(new List<object>(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));