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.
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));