ComboBox Binding to Custom ViewModel

2019-06-14 05:54发布

问题:

I am working on a WPF project. This is my XAML code:

<Window x:Class="MyNamespace.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:l="clr-namespace:MyNamespace" 
        xmlns:p="clr-namespace:MyNamespace.Properties"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
        Height="500" Title="{x:Static p:Resources.Title}" Width="500"
        WindowStartupLocation="CenterScreen">
    <Window.Resources>
        <l:BrowsersViewModel x:Key="BrowsersViewModel"/>
    </Window.Resources>
    <Canvas Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"
            DataContext="{StaticResource BrowsersViewModel}">
        <ComboBox Canvas.Left="10" Canvas.Top="10" DisplayMemberPath="Name"
                  ItemsSource="{Binding Path=Items}" 
                  SelectedItem="{Binding Mode=TwoWay, Path=SelectedItem}"
                  SelectedValuePath="Process" Width="379"/>
        <Button Content="Repopulate" Canvas.Right="10" 
                Canvas.Top="10" Width="75"/>
    </Canvas>
</Window>

And this is my ViewModel code:

// BrowserInstance is a simple struct with two public fields:
// 1) System.Diagnostics.Process Process
// 2) System.String Name
public sealed class BrowsersViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

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

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

    public BrowsersViewModel()
    {
        m_Items = new ObservableCollection<BrowserInstance>();
        Populate();
    }

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

    public void Populate()
    {
        foreach (Process process in Process.GetProcessesByName("chrome"))
        {
            BrowserInstance instance = new BrowserInstance();
            instance.Process = process;
            instance.Name = "[Chrome] " 
                         + process.Handle.ToString() 
                         + " " + ((process.MainWindowTitle.Length > 0) ? 
                                        process.MainWindowTitle : "NULL");

            m_Items.Add(instance);
        }

        NotifyPropertyChanged("Items");
    }
}

I'm having REALLY hard times getting this to work. I've looked at TONS of examples everywhere and I still can't find a solution to make everything work as expected.

1) I see a lot of values inside my ComboBox dropdown, but they are all empty. I would like to display the BrowserInstance.Name property inside the ComboBox and retrieve the BrowserInstance.Process value when an item is selected.

2) When the application starts, a check on the currently running browser processes is made in order to populate the ComboBox. If no running instances are found, how could I display a message inside my ComboBox like "No instances have been found!"?

3) If one or more browser instances are found when the application stats, how can I select the first one by default?

4) The Repopulate button will be used in order to recheck running browser instances by users. Let's say the previously selected instance is still running... how can I keep that one selected? And if the previously selected instances is not running anymore, how can I select by default the first one once again?

Many thanks!

EDIT: HERE IS MY CURRENT CODE

MainWindow:

public MainWindow()
{
    InitializeComponent();
    DataContext = m_BrowserInstances = new BrowserInstancesViewModel();
}

private void OnClickButtonRefresh(Object sender, RoutedEventArgs e)
{
    m_BrowserInstances.Populate();
}

BrowserInstancesViewModel:

public void Populate()
{
    BrowserInstance selectedItem = m_SelectedItem;
    List<BrowserInstance> items = new List<BrowserInstance>();

    foreach (Process process in Process.GetProcessesByName("chrome"))
        items.Add(new BrowserInstance(process));

    if (items.Count > 0)
    {
        m_Items = new ObservableCollection<BrowserInstance>(items.OrderBy(x => x.Process.Id));

        if ((selectedItem != null) && (m_Items.SingleOrDefault(NewMethod(selectedItem)) != null))
            m_SelectedItem = selectedItem;
        else
            m_SelectedItem = m_Items[0];

        m_Enabled = true;
    }
    else
    {
        m_Items = new ObservableCollection<BrowserInstance>() { (new BrowserInstance()) };
        m_SelectedItem = m_Items[0];

        m_Enabled = false;
    }

    NotifyPropertyChanged("Enabled");
    NotifyPropertyChanged("Items");
    NotifyPropertyChanged("SelectedItem");
}

回答1:

1) I can't replicate this issue, are you sure there's a running chrome process? :D

2) you can hack it like this: :D

     <ComboBox Canvas.Left="10" Canvas.Top="10" 
               DisplayMemberPath="Name" ItemsSource="{Binding Path=Items}"
               SelectedItem="{Binding Mode=TwoWay, Path=SelectedItem}" 
               SelectedValuePath="Process" Width="200">
        <ComboBox.Style>
            <Style TargetType="ComboBox">
                <Setter Property="IsEnabled" Value="True"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=Items.Count}" Value="0">
                        <Setter Property="IsEnabled" Value="False"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </ComboBox.Style>
    </ComboBox>
    <TextBlock Canvas.Left="10" Canvas.Top="10" 
               Text="No instances have been found!" >
        <TextBlock.Style>
            <Style TargetType="TextBlock">
                <Setter Property="Visibility" Value="Collapsed"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=Items.Count}" Value="0">
                        <Setter Property="Visibility" Value="Visible"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </TextBlock.Style>
    </TextBlock>

or better yet you retemplate the combobox to include textblock and bind its visibility using the hasitems property value of the combobox (ofcourse you will need to use BoolToVis converter)

3) add this code into the vm constructor after populate

if (m_Items.Count > 0)
{
    SelectedItem = m_Items[0];
}

4) add new repopulate method into the vm and store the current selecteditem, populate, check whether exist or not, reselect the item

    public void Repopulate()
    {
        BrowserInstance currentSelectedItem = m_SelectedItem;
        Populate();

        if (m_Items.Count>0)
        {
            if (currentSelectedItem !=null 
                && m_Items.FirstOrDefault
                   ((bi) => bi.Process == currentSelectedItem .Process) 
                         != null)
            {
                SelectedItem = currentSelectedItem;
            }
            else
            {
                SelectedItem = m_Items[0];
            }
        }
    }

NB:
I'm not sure what property to check to verify the existence of the instance becoz when i tried above code and debug it my chrome' process list keep changing. but basically thats what you could do



回答2:

BrowserInstance is a simple struct with two public fields:

WPF Does not support Data Binding to fields. Only Properties.

This should work:

public class BrowserInstance
{
    public string Name { get; set; }

    public Process Process { get; set; }
}