9

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>();
    }
}
  • It stopped working just after you altered the Template? Just notice something, try setting `IsSynchronizedWithCurrentItem = True` – trinaldi Dec 13 '13 at 22:54
  • Another thing: try using `ItemsPresenter x:Name="ItemsPresenter"`. (I have noticed that MSDN's documentation of Named Parts is riddled with mistakes.) – McGarnagle Dec 13 '13 at 23:18
  • Tico, if I put `True` to `IsSynchronizedWithCurrentItem`, doesn't work right, ComboBox doesn't select the right Item, just select the first. – BetaSystems - Rodrigo Duarte Dec 14 '13 at 00:33
  • McGarnagle, i'll try it. – BetaSystems - Rodrigo Duarte Dec 14 '13 at 00:33
  • None of the attempts worked. :P – BetaSystems - Rodrigo Duarte Dec 17 '13 at 17:24
  • You cannot define SelectedItem and SelectedValue both bindings together, combobox will not work correctly, there is a bug in combobox which makes it loose value of one of the properties. – Akash Kava Dec 20 '13 at 18:55
  • We're missing some info -- can you post the view-model property definition (and any relevant class definitions) for `OnwerTypes` and `Owner`? – McGarnagle Dec 20 '13 at 19:10
  • @McGarnagle, models added. – BetaSystems - Rodrigo Duarte Dec 20 '13 at 19:52
  • 1
    Thanks - should I assume you have all the `INofityPropertyChanged` in there, and left it out for brevity? – McGarnagle Dec 20 '13 at 19:58
  • 1
    One more typo: I think you must have `Style="{StaticResource ComboBoxTextBox}"` and not `Style="{x:Null}" Template="{StaticResource ComboBoxTextBox}"`. – McGarnagle Dec 20 '13 at 20:54
  • 1
    Is the `SelectedItem` the same reference in memory as one of the items in the `ItemsSource`? I recently answered [this similar question](http://stackoverflow.com/a/20706990/302677), and so this was the first thing that came to mind when reading your question. – Rachel Dec 20 '13 at 21:11
  • @McGarnagle, about `INotifyPropertyChanged`, you are right, it's there and I didn't insert because isn't necessary, and about `Style` or `Template`, need to be this, because I override the `ComboBoxTextBox` template, if I put in `Style` property, will be throw an error. – BetaSystems - Rodrigo Duarte Dec 27 '13 at 13:57
  • @Rachel, no, `ItemsSource` is a `List` and `SelectedItem` is `Owner.OwnerType`. – BetaSystems - Rodrigo Duarte Dec 27 '13 at 14:00
  • @BetaSystems-RodrigoDuarte That could be your problem then. Try binding `SelectedValue` instead of `SelectedItem`, or overwriting the `.Equals()` of `OwnerType` to ensure two objects are considered equal if their `Id` property matches. A third option is to set `Owner.OwnerType` with something like `OwnerTypes.FirstOrDefault(p => p.Id == Owner.OwnerTypeId)` so they're the same reference in memory – Rachel Dec 27 '13 at 14:11

3 Answers3

15

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.

Rachel
  • 130,264
  • 66
  • 304
  • 490
  • thanks for your answer, but I'm not sure that the way WPF works, because I use, in the whole project, the same way, `ItemsSource` and `SelectedItem` in different memory instances... As I update the question, in others part of the project, `SelectedItem` works, just on this one don't... – BetaSystems - Rodrigo Duarte Dec 27 '13 at 14:52
  • I couldn't know for sure unless I saw the other code that you have working, but the most common cases for that working is if you are binding to value types instead of reference types, or your `SelectedItem` gets set to an existing item from the `ItemsSource` collection instead of creating a new item. I know for sure this is the way WPF works though, because I've ran into this issue myself many times in the past :) – Rachel Dec 27 '13 at 15:33
  • Adding IEquatable to the data worked for me. – David Ehnis Apr 09 '21 at 14:21
1

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

Kumareshan
  • 1,311
  • 8
  • 8
1

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:

  1. The items in the list are retrieved from the property "OwnerTypes".
  2. The selected item will be bound to the property "OwnerType" inside the instance of the property "Owner".

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.

McGarnagle
  • 101,349
  • 31
  • 229
  • 260
  • As I said on update 4: `**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.` So, I understand your answer, but I'd like to keep working that way, if it's possible. – BetaSystems - Rodrigo Duarte Dec 20 '13 at 19:53
  • this way works, but isn't the right way, because I need to assign, or load, `OwnerType` when I commit to DB and with `SelectedValue` I don't set anything before I commit. – BetaSystems - Rodrigo Duarte Dec 20 '13 at 19:58
  • @BetaSystems-RodrigoDuarte but in update #3, the `SelectedValue` binding is `OneTime`, so of course that is not going to get updated. You only need to use `SelectedValue` or `SelectedItem`, not both of them. Just set the `OwnerTypeId` from the `Owner` setter (probably easiest), or vice versa. – McGarnagle Dec 20 '13 at 20:04
  • @BetaSystems-RodrigoDuarte I tried using both `SelectedValue` and `SelectedItem` and it seems to work -- see Edit #2 above. Could the problem just be that your Update #3 is using one-way bindings? – McGarnagle Dec 20 '13 at 20:11
  • that way it works, even in `OneWay` binding, but my doubt is why isn't working without `SelectedValue`? 'Cause all other ComboBox's works just with `SelectedItem`. – BetaSystems - Rodrigo Duarte Dec 23 '13 at 13:36