Tommaso Belluzzo
Tommaso Belluzzo

Reputation: 23675

Getting CRAZY with ComboBox SelectedItem

I have a data bound ComboBox that looks like this:

<ComboBox Canvas.Left="5" Canvas.Top="5" IsEnabled="{Binding Path=ComboBoxEnabled}" ItemsSource="{Binding Path=Items}" SelectedItem="{Binding Mode=TwoWay, Path=SelectedItem}" Width="250">
    <ComboBox.ItemTemplate>
        <DataTemplate>
            <TextBlock MaxWidth="{Binding Path=ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ComboBox}}}" Text="{Binding}" TextTrimming="CharacterEllipsis"/>
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>
<Label Canvas.Left="5" Canvas.Top="4" TextOptions.TextFormattingMode="Display" Content="No process instances have been found." Height="{DynamicResource {x:Static SystemParameters.WindowCaptionHeightKey}}" IsEnabled="False" Visibility="{Binding Path=WatermarkVisibility}" Width="250"/>
<Button Canvas.Right="5" Canvas.Top="5" Click="ClickRefresh" Content="Refresh" Width="75"/>

Then, in my MainWindow.xaml.cs:

public MainWindow()
{
    InitializeComponent();
    DataContext = m_ViewModel = new ViewModel();
}

private void ClickRefresh(Object sender, RoutedEventArgs e)
{
    m_ViewModel.Populate();
}

Here is my ViewModel.cs:

public ProcessInstance SelectedItem
{
    get { return m_SelectedItem; }
    set
    {
        if (m_SelectedItem != value)
        {
            m_SelectedItem = value;
            NotifyPropertyChanged("SelectedItem");
        }
    }
}

public ObservableCollection<ProcessInstance> Items
{
    get { return m_Items; }
    private set
    {
        if (m_Items != value)
        {
            m_Items = value;
            NotifyPropertyChanged("Items");
        }
    }
}

public ViewModel()
{
    Populate();
}

public void Populate()
{
    ProcessInstance selectedItem = m_SelectedItem;

    SelectedItem = null;
    Items = null;

    List<ProcessInstance> processInstances = new List<ProcessInstance>();

    foreach (Process process in Process.GetProcesses())
    {
        if (...)
            processInstances.Add(new ProcessInstance(process));
    }

    if (processInstances.Count == 0)
    {
        ComboBoxEnabled = false;
        WatermarkVisibility = Visibility.Visible;
    }
    else
    {
        Items = new ObservableCollection<ProcessInstance>(processInstances.OrderBy(x => x.Process.Id));

        if (selectedItem != null)
            SelectedItem = m_Items.SingleOrDefault(x => x.ProcessEquals(selectedItem));

        if (m_SelectedItem == null)
            SelectedItem = m_Items[0];

        ComboBoxEnabled = true;
        WatermarkVisibility = Visibility.Hidden;
    }
}

And here is my ProcessInstance class relevant code:

public override Boolean Equals(Object obj)
{
    return Equals(obj as ProcessInstance);
}

public override Int32 GetHashCode()
{
    Int32 hashCode;

    if ((m_Process == null) || m_Process.HasExited)
        hashCode = 0;
    else
    {
        hashCode = (m_Process.Id.GetHashCode() * 397) ^ m_Process.MainModule.BaseAddress.GetHashCode();

        if (!String.IsNullOrEmpty(m_Process.MainWindowTitle))
            hashCode = (hashCode * 397) ^ m_Process.MainWindowTitle.GetHashCode();
    }

    return hashCode;
}

public override String ToString()
{
    String processId = process.Id.ToString("X8", CultureInfo.CurrentCulture);
    String windowTitle = (process.MainWindowTitle.Length > 0) ? process.MainWindowTitle : "NULL";

    return String.Format(CultureInfo.CurrentCulture, "[{0}] {1} - {2}", type, processId, windowTitle);
}

public Boolean Equals(ProcessInstance other)
{
    if (other == null)
        return false;

    if (ReferenceEquals(this, other))
        return true;

    if (m_Process == null)
    {
        if (other.Process == null)
            return true;

        return false;
    }

    if (other.Process == null)
        return false;

    return ((m_Process.Id == other.Process.Id) && (m_Process.MainModule.BaseAddress == other.Process.MainModule.BaseAddress) && (m_Process.MainWindowTitle == other.Process.MainWindowTitle));
}

public Boolean ProcessEquals(ProcessInstance other)
{
    if (other == null)
        throw new ArgumentNullException("other");

    if (m_Process == null)
        return (other.Process == null);

    if (other.Process == null)
        return false;

    return ((m_Process.Id == other.Process.Id) && (m_Process.MainModule.BaseAddress == other.Process.MainModule.BaseAddress));
}

Now here is what happens... I start the application while no process instances exist:

enter image description here

Then I open one or more process instances and I click the Refresh button. The first one is selected by ComboBox as default... I can keep that one selected or select another one, it doesn't matter:

enter image description here

Now i close every process instance and I click Refresh button again. What happens in this case is that the Populate() method sets both SelectedItem and Items to null, so the ComboBox looks empty, and then it disables the ComboBox and makes the watermark Label visible. But here is what I get:

enter image description here

The previous SelectedItem is still there. Why? WHY?!?!

[EDIT] HERE IS A LINK TO DOWNLOAD THE PROJECT:

http://www.filedropper.com/damncb

Upvotes: 3

Views: 713

Answers (4)

Rachel
Rachel

Reputation: 132548

I'm unable to run your test project because of the version, but I copied the ViewModel class and XAML to a new test project and it all seems to work as expected, with the selected item being cleared, combobox disabling, and watermark appearing when the button is clicked.

So I would guess that either something in your environment is affecting it, or it is something else that isn't immediately apparent with your code.

To find out which, I have posted the (correctly working) code that I am using to test below.

If this doesn't work, then the problem has something to do with your environment. I'm using VS2010, .Net 4.0, and Windows 7.

If it does, then you must have something occurring elsewhere in your code that is not immediately apparent in your test examples. I would check for any async code to see if the PropertyChange notifications are occurring on a background thread, as sometimes that doesn't trigger the update on the UI thread.

I apologize in advance for the resulting code dump :)

The ViewModel class was copied from test project, and a find/replace done to change "BrowserInstance" to "ViewModelBase" to fit my testing app. Some unnecessary sections were also commented out or modified slightly to accommodate my test app.

public sealed class ViewModel : INotifyPropertyChanged
{
    #region Members: Fields
    private Boolean m_ButtonAttachEnabled;
    private Boolean m_ButtonDetachEnabled;
    private Boolean m_ButtonRefreshEnabled;
    private Boolean m_ComboBoxEnabled;
    private Boolean m_ProgressBarEnabled;
    private ViewModelBase m_SelectedItem;
    private Dictionary<String, PropertyChangedEventArgs> m_PropertyChangedEventArgs = new Dictionary<String, PropertyChangedEventArgs>();
    private Double m_ProgressBarMaximum;
    private Double m_ProgressBarValue;
    private IntPtr m_ProcessHandle;
    private IntPtr m_ProcessHook;
    private ObservableCollection<ViewModelBase> m_Items;
    private Visibility m_WatermarkVisibility;
    #endregion

    #region Members: INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    #endregion

    #region Properties
    public Boolean ButtonAttachEnabled
    {
        get { return m_ButtonAttachEnabled; }
        private set
        {
            if (m_ButtonAttachEnabled != value)
            {
                m_ButtonAttachEnabled = value;
                NotifyPropertyChanged("ButtonAttachEnabled");
            }
        }
    }

    public Boolean ButtonDetachEnabled
    {
        get { return m_ButtonDetachEnabled; }
        private set
        {
            if (m_ButtonDetachEnabled != value)
            {
                m_ButtonDetachEnabled = value;
                NotifyPropertyChanged("ButtonDetachEnabled");
            }
        }
    }

    public Boolean ButtonRefreshEnabled
    {
        get { return m_ButtonRefreshEnabled; }
        private set
        {
            if (m_ButtonRefreshEnabled != value)
            {
                m_ButtonRefreshEnabled = value;
                NotifyPropertyChanged("ButtonRefreshEnabled");
            }
        }
    }

    public Boolean ComboBoxEnabled
    {
        get { return m_ComboBoxEnabled; }
        private set
        {
            if (m_ComboBoxEnabled != value)
            {
                m_ComboBoxEnabled = value;
                NotifyPropertyChanged("ComboBoxEnabled");
            }
        }
    }

    public Boolean ProgressBarEnabled
    {
        get { return m_ProgressBarEnabled; }
        private set
        {
            if (m_ProgressBarEnabled != value)
            {
                m_ProgressBarEnabled = value;
                NotifyPropertyChanged("ProgressBarEnabled");
            }
        }
    }

    public ViewModelBase SelectedItem
    {
        get { return m_SelectedItem; }
        set
        {
            if (m_SelectedItem != value)
            {
                m_SelectedItem = value;
                NotifyPropertyChanged("SelectedItem");
            }
        }
    }

    public Double ProgressBarMaximum
    {
        get { return m_ProgressBarMaximum; }
        private set
        {
            if (m_ProgressBarMaximum != value)
            {
                m_ProgressBarMaximum = value;
                NotifyPropertyChanged("ProgressBarMaximum");
            }
        }
    }

    public Double ProgressBarValue
    {
        get { return m_ProgressBarValue; }
        private set
        {
            if (m_ProgressBarValue != value)
            {
                m_ProgressBarValue = value;
                NotifyPropertyChanged("ProgressBarValue");
            }
        }
    }

    public ObservableCollection<ViewModelBase> Items
    {
        get { return m_Items; }
        private set
        {
            if (m_Items != value)
            {
                m_Items = value;
                NotifyPropertyChanged("Items");
            }
        }
    }

    public Visibility WatermarkVisibility
    {
        get { return m_WatermarkVisibility; }
        private set
        {
            if (m_WatermarkVisibility != value)
            {
                m_WatermarkVisibility = value;
                NotifyPropertyChanged("WatermarkVisibility");
            }
        }
    }
    #endregion

    #region Constructors
    public ViewModel()
    {
        m_PropertyChangedEventArgs = new Dictionary<String, PropertyChangedEventArgs>();
        Populate();
    }
    #endregion

    #region Methods: Functions
    private PropertyChangedEventArgs GetPropertyChangedEventArgs(String propertyName)
    {
        PropertyChangedEventArgs propertyChangedEventArgs;

        if (!m_PropertyChangedEventArgs.TryGetValue(propertyName, out propertyChangedEventArgs))
            m_PropertyChangedEventArgs[propertyName] = propertyChangedEventArgs = new PropertyChangedEventArgs(propertyName);

        return propertyChangedEventArgs;
    }

    private void NotifyPropertyChanged(String propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, GetPropertyChangedEventArgs(propertyName));
    }

    private void ResetProgressBar(Int32 maximumValue = 100)
    {
        ProgressBarMaximum = maximumValue;
        ProgressBarValue = 0;
    }

    private void SetInterfaceStatus(Status status)
    {
        switch (status)
        {
            case Status.Attached:
                {
                    ButtonAttachEnabled = false;
                    ButtonDetachEnabled = true;
                    ButtonRefreshEnabled = false;
                    ComboBoxEnabled = false;
                    ProgressBarEnabled = false;
                    WatermarkVisibility = Visibility.Hidden;

                    break;
                }

            case Status.Busy:
                {
                    ButtonAttachEnabled = false;
                    ButtonDetachEnabled = false;
                    ButtonRefreshEnabled = false;
                    ComboBoxEnabled = false;
                    ProgressBarEnabled = true;
                    WatermarkVisibility = Visibility.Hidden;

                    break;
                }

            case Status.Detached:
                {
                    ButtonAttachEnabled = true;
                    ButtonDetachEnabled = false;
                    ButtonRefreshEnabled = true;
                    ComboBoxEnabled = true;
                    ProgressBarEnabled = true;
                    WatermarkVisibility = Visibility.Hidden;

                    goto default;
                }

            case Status.DetachedEmpty:
                {
                    ButtonAttachEnabled = false;
                    ButtonDetachEnabled = false;
                    ButtonRefreshEnabled = true;
                    ComboBoxEnabled = false;
                    ProgressBarEnabled = false;
                    WatermarkVisibility = Visibility.Visible;

                    goto default;
                }

            default:
                ResetProgressBar();
                break;
        }
    }

    public void Attach()
    {
    }

    public void Detach()
    {
    }

    public void Populate()
    {
        ViewModelBase selectedItem = m_SelectedItem;

        SelectedItem = null;
        Items = new ObservableCollection<ViewModelBase>();

        List<ViewModelBase> ViewModelBases = new List<ViewModelBase>();

        if (selectedItem == null)
        {
            ViewModelBases.Add(new Test { TestValue = "123456789", TestEnum = TestEnum.A, TestBool = false, TestBool2 = true });
            ViewModelBases.Add(new Test { TestValue = "987365321", TestEnum = TestEnum.B, TestBool = true, TestBool2 = true });
            ViewModelBases.Add(new Test { TestValue = "784512457", TestEnum = TestEnum.B, TestBool = true, TestBool2 = true });
        }

        //foreach (Process process in Process.GetProcesses())
        //{
        //    if ((process.ProcessName == "chrome") && !process.HasExited && (process.MainModule.ModuleName == "chrome.exe"))
        //        ViewModelBases.Add(new ViewModelBase(ViewModelBaseType.Chrome, process));
        //}

        if (ViewModelBases.Count == 0)
            SetInterfaceStatus(Status.DetachedEmpty);
        else
        {
            Items = new ObservableCollection<ViewModelBase>(ViewModelBases);

            if (selectedItem != null)
                SelectedItem = m_Items.SingleOrDefault(x => x.Equals(selectedItem));

            if (m_SelectedItem == null)
                SelectedItem = m_Items[0];

            SetInterfaceStatus(Status.Detached);
        }
    }
    #endregion

    #region Enumerators
    private enum Status
    {
        Attached,
        Busy,
        Detached,
        DetachedEmpty
    }
    #endregion
}

The ViewModelBase class from my test app looks like this:

public class ViewModelBase : INotifyPropertyChanged, IDataErrorInfo
{
    // Fields
    private PropertyChangedEventHandler propertyChanged;

    // Events
    public event PropertyChangedEventHandler PropertyChanged
    {
        add
        {
            PropertyChangedEventHandler handler2;
            PropertyChangedEventHandler propertyChanged = this.propertyChanged;
            do
            {
                handler2 = propertyChanged;
                PropertyChangedEventHandler handler3 = (PropertyChangedEventHandler)Delegate.Combine(handler2, value);
                propertyChanged = Interlocked.CompareExchange<PropertyChangedEventHandler>(ref this.propertyChanged, handler3, handler2);
            }
            while (propertyChanged != handler2);
        }
        remove
        {
            PropertyChangedEventHandler handler2;
            PropertyChangedEventHandler propertyChanged = this.propertyChanged;
            do
            {
                handler2 = propertyChanged;
                PropertyChangedEventHandler handler3 = (PropertyChangedEventHandler)Delegate.Remove(handler2, value);
                propertyChanged = Interlocked.CompareExchange<PropertyChangedEventHandler>(ref this.propertyChanged, handler3, handler2);
            }
            while (propertyChanged != handler2);
        }
    }

    protected void RaisePropertyChanged(params string[] propertyNames)
    {
        if (propertyNames == null)
        {
            throw new ArgumentNullException("propertyNames");
        }
        foreach (string str in propertyNames)
        {
            this.RaisePropertyChanged(str);
        }
    }

    protected void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
    {
        string propertyName = PropertySupport.ExtractPropertyName<T>(propertyExpression);
        this.RaisePropertyChanged(propertyName);
    }

    protected virtual void RaisePropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler propertyChanged = this.propertyChanged;
        if (propertyChanged != null)
        {
            propertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #region IDataErrorInfo & Validation Members

    /// <summary>
    /// List of Property Names that should be validated
    /// </summary>
    //protected List<string> ValidatedProperties = new List<string>();


    #region Validation Delegate

    public delegate string ValidationDelegate(
        object sender, string propertyName);

    private List<ValidationDelegate> _validationDelegates =
        new List<ValidationDelegate>();

    public void AddValidationDelegate(ValidationDelegate func)
    {
        _validationDelegates.Add(func);
    }

    public void RemoveValidationDelegate(ValidationDelegate func)
    {
        if (_validationDelegates.Contains(func))
            _validationDelegates.Remove(func);
    }

    #endregion // Validation Delegate


    public virtual string GetValidationError(string propertyName)
    {
        // If user specified properties to validate, check to see if this one exists in the list
        //if (ValidatedProperties.IndexOf(propertyName) < 0)
        //{
        //    return null;
        //}

        string s = null;

        //switch (propertyName)
        //{

        //}

        foreach (var func in _validationDelegates)
        {
            s = func(this, propertyName);
            if (s != null)
                return s;
        }


        return s;
    }

    string IDataErrorInfo.Error { get { return null; } }

    string IDataErrorInfo.this[string propertyName]
    {
        get { return this.GetValidationError(propertyName); }
    }

    //public bool IsValid
    //{
    //    get
    //    {
    //        return (GetValidationError() == null);
    //    }
    //}

    //public string GetValidationError()
    //{
    //    string error = null;

    //    if (ValidatedProperties != null)
    //    {
    //        foreach (string s in ValidatedProperties)
    //        {
    //            error = GetValidationError(s);
    //            if (error != null)
    //            {
    //                return error;
    //            }
    //        }
    //    }

    //    return error;
    //}

    #endregion // IDataErrorInfo & Validation Members
}

And the Test class for testing is this:

public class Test : ViewModelBase
{
    public int Id { get; set; }
    private TestEnum _testEnum;
    private string _testValue = "Testing";
    private double _testNumber = 10000;
    private bool _testBool;
    private bool _testBool2;

    private ObservableCollection<int> _someCollection;

    public ObservableCollection<int> SomeCollection
    {
        get
        {
            if (_someCollection == null)
                _someCollection = new ObservableCollection<int>();

            return _someCollection;
        }
    }

    public Test()
    {
        //this.ValidatedProperties.Add("TestValue");
        //this.ValidatedProperties.Add("TestNumber");

        SomeCollection.Add(1);
        SomeCollection.Add(2);
        SomeCollection.Add(3);
    }

    public override string ToString()
    {
        return TestValue;
    }


    public void RaisePropertyChanged1(string property)
    {
        base.RaisePropertyChanged(property);
    }

    //public override string GetValidationError(string propertyName)
    //{

    //    // If user specified properties to validate, check to see if this one exists in the list
    //    if (ValidatedProperties.IndexOf(propertyName) < 0)
    //    {
    //        return null;
    //    }

    //    string s = base.GetValidationError(propertyName); ;

    //    switch (propertyName)
    //    {
    //        case "TestValue":
    //            s = "error";
    //            break;
    //    }

    //    return s;

    //}



    public TestEnum TestEnum
    {
        get { return _testEnum; }
        set
        {
            if (value != _testEnum)
            {
                _testEnum = value;
                base.RaisePropertyChanged(() => this.TestEnum);
            }
        }
    }

    private ObservableCollection<int> _test;

    public ObservableCollection<int> Test1
    {
        get
        {
            if (_test == null)
            {
                _test = new ObservableCollection<int>();
                for (int i = 0; i < 5; i++)
                {
                    _test.Add(i);
                }
            }
            return _test;
        }
        set
        {
            if (value != _test)
            {
                _test = value;
                RaisePropertyChanged(() => this.Test1);
            }
        }
    }

    public string TestValue
    {
        get { return _testValue; }
        set
        {
            if (value != _testValue)
            {
                _testValue = value;
                base.RaisePropertyChanged(() => this.TestValue);
            }
        }
    }

    public bool TestBool
    {
        get { return _testBool; }
        set
        {
            if (value != _testBool)
            {
                _testBool = value;
                base.RaisePropertyChanged(() => this.TestBool);
            }
        }
    }

    public bool TestBool2
    {
        get { return _testBool2; }
        set
        {
            if (value != _testBool2)
            {
                _testBool2 = value;
                base.RaisePropertyChanged(() => this.TestBool);
            }
        }
    }

    public double TestNumber
    {
        get { return _testNumber; }
        set
        {
            if (value != _testNumber)
            {
                _testNumber = value;
                base.RaisePropertyChanged(() => this.TestNumber);
            }
        }
    }
}

My test XAML was looked like this. I had added a min-height because it wasn't showing up correctly with my test styles, and a background color was added just to see where it was rendering at.

<Grid MinHeight="100" Background="Lavender">
    <Canvas Background="Red">
        <GroupBox Canvas.Left="10" Canvas.Top="5" BorderBrush="Black" Header="Test" Height="86" Width="472">
            <Canvas>
                <ComboBox Canvas.Left="5" Canvas.Top="5" IsEnabled="{Binding Path=ComboBoxEnabled}" ItemsSource="{Binding Path=Items}" SelectedItem="{Binding Mode=TwoWay, Path=SelectedItem}" Width="366">
                    <ComboBox.ItemTemplate>
                        <DataTemplate>
                            <TextBlock MaxWidth="{Binding Path=ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ComboBox}}}" Text="{Binding }" TextTrimming="CharacterEllipsis"/>
                        </DataTemplate>
                    </ComboBox.ItemTemplate>
                </ComboBox>
                <Label Canvas.Left="5" Canvas.Top="4" TextOptions.TextFormattingMode="Display" Content="This is a Test" Height="50" IsEnabled="False" Visibility="{Binding Path=WatermarkVisibility}" Width="366"/>
                <Button Canvas.Right="5" Canvas.Top="5" Click="Button_Click_1" Content="Refresh" IsEnabled="{Binding Path=ButtonRefreshEnabled}" Width="75"/>
            </Canvas>
        </GroupBox>
    </Canvas>
</Grid>

And last of all, the code behind my XAML:

ViewModel m_ViewModel;

public MainWindow()
{
    InitializeComponent();
    DataContext = m_ViewModel = new ViewModel();
}

private void Button_Click_1(object sender, RoutedEventArgs e)
{
    m_ViewModel.Populate();
}

Upvotes: 1

ncourcy84
ncourcy84

Reputation: 101

You are not clearing your ObservableCollection. Try this :

if (processInstances.Count == 0)
{
    Items.Clear();

    // remaining of your logic;
}

Upvotes: 1

Dean Chalk
Dean Chalk

Reputation: 20451

You need to clear the SelectedValue property on the ComboBox, or maybe set SelectedIndex to -1

Upvotes: 3

Harsh Baid
Harsh Baid

Reputation: 7249

Few points to hit the solution

  1. Try clearing the values instead of nullifying them, so that WPF UI elements would not loose the binding to the instance when PropertyChanged events are triggered..
  2. Based on point 1, Set the SelectedItem internals to value '' for the Text property or whatsoever you have wrapped instead of nullifying entire instance and recreating new each time.
  3. Hmm.. You have not shown how the combobox.selectedItem is getting the ToString() or something similar from the wrapper class ProcessInstance of your so I could not post the change of logic from code perspective.

Upvotes: 1

Related Questions