I already search widely, but I can't find any solution to my case.
I have several ComboBox's at my project and I was searching for an AutoComplete solution, then I found a good one and applied in my project, I applied solution's style as well to all ComboBox in my project.
After that, SelectedItem stopped working, some one can help me ?
My Combobox:
<ComboBox Name="CbOwnerType" Grid.Column="1" Grid.Row="2" ItemsSource="{Binding Path=OwnerTypes, Mode=OneWay}" SelectedItem="{Binding Owner.OwnerType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValuePath="Id" DisplayMemberPath="Name" Margin="5,0,10,0" />
My style:
<Style TargetType="{x:Type ComboBox}">
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="FontWeight" Value="ExtraBold" />
<Setter Property="IsEditable" Value="False"/>
<Setter Property="IsSynchronizedWithCurrentItem" Value="False" />
<Setter Property="StaysOpenOnEdit" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBox}">
<Grid>
<ToggleButton Name="ToggleButton" Template="{StaticResource ComboBoxToggleButton}" Grid.Column="2" Focusable="True" IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press" BorderThickness="0" />
<ContentPresenter Name="ContentSite" IsHitTestVisible="False" Content="{TemplateBinding SelectionBoxItem}" ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" Margin="5,0,20,0" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBox x:Name="PART_EditableTextBox" Style="{x:Null}" Template="{StaticResource ComboBoxTextBox}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="3,3,23,3" Focusable="True" Background="Transparent" Visibility="Hidden" IsReadOnly="{TemplateBinding IsReadOnly}" />
<Popup Name="Popup" Placement="Bottom" IsOpen="{TemplateBinding IsDropDownOpen}" AllowsTransparency="True" Focusable="False" PopupAnimation="Slide">
<Themes:SystemDropShadowChrome Margin="4,6,4,6" CornerRadius="4">
<Grid Name="DropDown" SnapsToDevicePixels="True" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}">
<Border x:Name="DropDownBorder" Background="{StaticResource WindowBackgroundBrush}" BorderThickness="1" BorderBrush="{StaticResource SolidBorderBrush}" />
<ScrollViewer Margin="4,6,4,6" SnapsToDevicePixels="True">
<ItemsPresenter />
</ScrollViewer>
</Grid>
</Themes:SystemDropShadowChrome>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="HasItems" Value="false">
<Setter TargetName="DropDownBorder" Property="MinHeight" Value="95"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}"/>
</Trigger>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</Trigger>
<Trigger SourceName="Popup" Property="Popup.AllowsTransparency" Value="true">
<Setter TargetName="DropDownBorder" Property="CornerRadius" Value="4"/>
<Setter TargetName="DropDownBorder" Property="Margin" Value="0,2,0,0"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
UPDATE
My ToggleButton
<ControlTemplate x:Key="ComboBoxToggleButton" TargetType="{x:Type ToggleButton}" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<Border x:Name="Border" Grid.ColumnSpan="2" BorderBrush="{StaticResource LabPetsStandardColor}" BorderThickness="1" CornerRadius="5" />
<Border Grid.Column="0" Margin="1" Background="Transparent" BorderBrush="{StaticResource NormalBorderBrush}" BorderThickness="0" CornerRadius="5,0,0,5" />
<Path x:Name="Arrow" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" Data="M 0 0 L 4 4 L 8 0 Z">
<Path.Fill>
<SolidColorBrush Color="Black" />
</Path.Fill>
</Path>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="ToggleButton.IsMouseOver" Value="true">
<Setter TargetName="Border" Property="Background" Value="{StaticResource LabPetsStandardColor}" />
</Trigger>
<Trigger Property="ToggleButton.IsChecked" Value="true">
<Setter TargetName="Border" Property="Background" Value="{StaticResource LabPetsPressedStandardColor}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border" Property="Background" Value="{StaticResource DisabledBackgroundBrush}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource DisabledBorderBrush}" />
<Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}"/>
<Setter TargetName="Arrow" Property="Fill" Value="{StaticResource DisabledForegroundBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
My TextBox
<Style x:Key="ComboBoxTextBox" TargetType="{x:Type TextBox}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="AllowDrop" Value="True" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" Background="#00FFFFFF" Name="PART_ContentHost" Focusable="False" VerticalAlignment="Center" VerticalContentAlignment="Center" Margin="0">
<ScrollViewer.Style>
<Style TargetType="ScrollViewer">
<Setter Property="OverridesDefaultStyle" Value="True" />
</Style>
</ScrollViewer.Style>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Some, please, can help me ?
UPDATE 2
Found a hack, not the perfect solution, but kind of works...
If I insert property SelectedValue
and the value Owner.OwnerTypeId
, it works like a charm... But, it's right this?
My Combobox now:
<ComboBox Name="CbOwnerType" Grid.Column="1" Grid.Row="2" ItemsSource="{Binding Path=OwnerTypes, Mode=OneWay}" SelectedItem="{Binding Owner.OwnerType}" SelectedValue="{Binding Owner.OwnerTypeId}" SelectedValuePath="Id" DisplayMemberPath="Name" Margin="5,0,10,0" />
This is a solution, not that I like it, but it's a solution...
Some one can answer why SelectedItem
isn't working as it should ?
Obs.: When I change the selection, SelectedItem
works, just doesn't work when I load my view.
UPDATE 3
Ok, it's worked like I said, but the problem is that WPF is hitting 4 times at my ViewModel, so I changed my ComboBox
a little:
<ComboBox Name="CbOwnerType" Grid.Column="1" Grid.Row="2" ItemsSource="{Binding Path=OwnerTypes, Mode=OneWay}" SelectedItem="{Binding Owner.OwnerType}" SelectedValue="{Binding Owner.OwnerTypeId, Mode=OneTime}" SelectedValuePath="Id" DisplayMemberPath="Name" Margin="5,0,10,0" />
So, now, WPF just search the OwnerTypeId
and when I change an item, WPF just hits 2 times.
UPDATE 4
Ok, another strange finding...
In another ComboBox, with the same properties, except SelectedValue
, it's working perfect... I can't understand what is happening.
UPDATE 5
Sorry about that, I forgot to post my models.
Model Owner:
public class Owner
{
public int Id { get; set; }
public int OwnerTypeId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string FormatedPhone
{
get
{
if (this.Phone == null)
return string.Empty;
switch (this.Phone.Length)
{
case 11:
return Regex.Replace(this.Phone, @"(\d{2})(\d{4})(\d{4})", "($1) $2-$3");
case 12:
return Regex.Replace(this.Phone, @"(\d{2})(\d{5})(\d{4})", "($1) $2-$3");
default:
return this.Phone;
}
}
}
public string Phone { get; set; }
public string CellPhone { get; set; }
public string FormatedCellPhone
{
get
{
if (this.CellPhone == null)
return string.Empty;
switch (this.CellPhone.Length)
{
case 11:
return Regex.Replace(this.Phone, @"(\d{2})(\d{4})(\d{4})", "($1) $2-$3");
case 12:
return Regex.Replace(this.Phone, @"(\d{2})(\d{5})(\d{4})", "($1) $2-$3");
default:
return this.CellPhone;
}
}
}
public string Email { get; set; }
public virtual OwnerType OwnerType { get; set; }
public virtual ICollection<Animal> Animals { get; set; }
public Owner()
{
this.OwnerType = new OwnerType();
this.Animals = new List<Animal>();
this.ErrorList = new StringBuilder();
}
Model OwnerType:
public class OwnerType
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Owner> Owners { get; set; }
public OwnerType()
{
this.Owners = new List<Owner>();
}
}
Your SelectedItem
binding isn't working because WPF compares the SelectedItem
to the items in the ItemsSource
by the .Equals()
method, which by default compares by reference. And the instance in memory containing your SelectedItem
is not the same instance in memory as one of the items in your ItemsSource
There are 3 ways to handle this.
First, as you've already discovered you can bind SelectedValue
to a value type property on your item, and set SelectedValuePath
.
<ComboBox ItemsSource="{Binding Path=OwnerTypes}"
SelectedValue="{Binding Owner.OwnerTypeId}"
SelectedValuePath="Id" DisplayMemberPath="Name"
/>
This is usually the solution I go with because it's often easiest
Second, you could ensure your SelectedItem
is set to the same reference in memory as one of the ItemsSource
items. Depending on the application design, this isn't a bad choice either.
public class Owner()
{
public int OwnerTypeId { get; set; }
public OwnerType OwnerType
{
get
{
return StaticClass.OwnerTypes
.FirstOrDefault(p => p.Id == this.OwnerTypeId);
}
set
{
if (value != null)
OwnerTypeId = value.Id;
}
}
}
And last of all, you could override the .Equals()
method on the OwnerType
object so it considers the two values equal if the Id
properties are the same.
public override bool Equals(object obj)
{
if (obj == null || !(obj is OwnerType))
return false;
return ((OwnerType)obj).Id == this.Id);
}
I usually try to avoid this method unless I know I'll always want to compare this object's equality by the Id
property only, but sometimes this is the best way to go.
Also, its good practice to override .GetHashCode()
whenever you overwrite .Equals()
.
As a side note, you usually don't want to be binding both SelectedItem
and SelectedValue
. They are two different ways of setting the same thing, and you can get unexpected results by binding both of them.
You have to do a Template binding of selectedValue of comobox. Because you have overriden the control template and placed your own TextBlock to display the selected value. Now when the value is selected from the UI control Template takes care of displaying it. but it is not setting the value to the SelectedValue or SelectedItem.
<TextBox x:Name="PART_EditableTextBox" Style="{x:Null}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="3,3,23,3" Focusable="True" Background="Transparent" Visibility="Hidden" IsReadOnly="{TemplateBinding IsReadOnly}" Text="{TemplateBinding SelectedValue}"/>
Hope this helps you
Let's look at the bindings, and break down what they say:
<ComboBox ItemsSource="{Binding Path=OwnerTypes, Mode=OneWay}"
SelectedItem="{Binding Owner.OwnerType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Id" DisplayMemberPath="Name"
/>
So this means:
Since there is no binding applied to SelectedValue
, the SelectedValuePath
will be completely ignored. Note the definition of SelectedValuePath:
Gets or sets the path that is used to get the SelectedValue from the SelectedItem.
So let's omit #3 for now. Working with #1 and #2, let's assume that the property "OwnerTypes" is of type List<OwnerTypeDef>
. This implies that the type OwnerTypeDef
is the same type as the property "OwnerType" inside "Owner" property. With this setup --
public class OwnerDef : INotifyPropertyChanged // TODO implement INotifyPropertyChanged
{
public OwnerTypeDef OwnerType
{
get { return _ownerType; }
set
{
if (_ownerType == value)
return;
_ownerType = value;
RaisePropertyChanged();
}
}
private OwnerTypeDef _ownerType;
}
public class OwnerTypeDef : INotifyPropertyChanged
{
public string Name
{
get { return _name; }
set
{
if (_name == value)
return;
_name = value;
RaisePropertyChanged();
}
}
private string _name;
public int Id
{
get { return _id; }
set
{
if (_id == value)
return;
_id = value;
RaisePropertyChanged();
}
}
private int _id;
}
public class ViewModel : INotifyPropertyChanged
{
public List<OwnerTypeDef> OwnerTypes
{
get { return _ownerTypes; }
set { _ownerTypes = value; }
}
private List<OwnerTypeDef> _ownerTypes = new List<OwnerTypeDef>
{
new OwnerTypeDef { Name = "foo", Id = 1, },
new OwnerTypeDef { Name = "bar", Id = 2, },
new OwnerTypeDef { Name = "baz", Id = 3, },
};
public OwnerDef Owner
{
get { return _owner; }
set
{
if (_owner == value)
return;
_owner = value;
RaisePropertyChanged();
}
}
private OwnerDef _owner = new OwnerDef();
}
-- the binding works for me. The property "OwnerType" inside "Owner" gets updated when I change the selection in the UI.
Edit
Let's look at the other scenario, where you use SelectedValuePath
and SelectedValue
. In this case, we are binding the selected value to "Owner.OwnerTypeId" (an integer). We'll use SelectedValuePath=Id
, which tells the framework to look for the "Id" property in the selected item (which is OwnerDef.Id
, an int
). Therefore, we'll have to add a matching int
property in the OwnerDef
class, call it "OwnerTypeId". XAML in this case would be:
<ComboBox ItemsSource="{Binding Path=OwnerTypes, Mode=OneWay}"
SelectedValue="{Binding Owner.OwnerTypeId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Id" DisplayMemberPath="Name"
/>
With this setup the binding is updating the "OwnerTypeId" correctly.
Edit #2
It's also possible use both SelectedItem
and SelectedValue
, so that both the "OwnerType" and "OwnerTypeID" properties get updated. Make sure to use TwoWay
bindings (in Update #3 above they are the default OneWay):
<ComboBox ItemsSource="{Binding OwnerTypes}"
SelectedValuePath="Id" SelectedValue="{Binding Owner.OwnerTypeId,Mode=TwoWay}"
SelectedItem="{Binding Owner.OwnerType,Mode=TwoWay}"
DisplayMemberPath="Name"
/>
This setup updates both the "OwnerTypeId" and the "OwnerType" properties when I select an item in the combo box.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With