Data Binding Custom Class to WPF ComboBox Using DisplayMemberPath - c#

I know this is basic, but I'm making the jump from vb.net to C#, and the approach I was using in vb.net doesn't seem to be working.
I've created a .dll with a custom class Service.
In my project, I'm populating an ObservableCollection with instances of Service. I want to display the instances in a combobox using DisplayMemberPath in XAML (WPF).
My instances of Service are populating the ComboBox, but the display for each item is blank; I'm just getting a bunch of blank lines to choose from.
I've tried this with and without implementing INotifyPropertyChanged on the class itself, although I don't think it should be necessary at this point since I'm still pretty much at square 1.
Here's my code:
<Grid>
<ComboBox Name="TopService"
VerticalAlignment="Top"
ItemsSource="{Binding}"
DisplayMemberPath="{Binding ServCode}"></ComboBox>
</Grid>
And here's my code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Execute();
}
private void Execute()
{
SA.franchiseID = "HOL010";
ObservableCollection<Service> ocService = Service.InitializeServiceList();
TopService.DataContext = ocService;
}
}
And the code for the class (referenced via .dll)
public class Service : INotifyPropertyChanged
{
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
protected void Notify(string propertyName)
{
if (this.PropertyChanged != null)
{ PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); }
}
#endregion
private string servCode;
public string ServCode
{
get { return servCode; }
set { servCode = value; Notify("ServCode"); }
}
public string programCode = "";
public int roundNum = 0;
public static ObservableCollection<Service> InitializeServiceList()
{
ObservableCollection<Service> oc = new ObservableCollection<Service>();
using (SA s = new SA())
{
s.SqlString = #"select
ps.serv_code
,pc.prgm_code
,ps.round
from prgmserv as ps
inner join prgmcd as pc on pc.progdefid = ps.progdefid
where pc.available = 1";
s.reader = s.command.ExecuteReader();
Service newService;
while (s.reader.Read())
{
newService = new Service();
newService.servCode = s.reader["serv_code"].ToString();
newService.programCode = s.reader["prgm_code"].ToString();
newService.roundNum = Convert.ToInt32(s.reader["round"].ToString());
oc.Add(newService);
newService = null;
}
return oc;
}
}
}

DisplayMemberPath is a string. You don't give it a binding to a property; you just give it a path to a property, which it then looks up by reflection.
Try this:
DisplayMemberPath="ServCode"
What you were doing would make it use the value of ServCode as a DisplayMemberPath; if ServCode is 12, it would look for a property named 12 on each item in the combobox -- not your intent, I'm sure.

I have also come to realize while attempting to bind items to an ItemsControl, that the difference between a Field and a Property become very important. I have been trying to bind to Fields, which does not work. This is technically not different than in VB.net. But an implicitly defined Property in VB.net looks very much like a Field in C#. All that said, I believe I'm ready to go conquer the world now!

Related

WPF: Binding is lost when bindings are updated

I have a WPF-Application for controlling a WCF-RESTful service, i.e. for starting, initializing and stopping it. Therefore I have a MainWindow UI which contains a UserControl to configure settings. When I initialize my service, some data is loaded into DependencyProperties and ObservableCollections to display it in the GUI. Here is the part of the method where I update these settings:
public partial class MainWindow : Window {
private void InitializeService (bool reInitialize = false) {
var restService = (RestService)this.ServiceHost.SingletonInstance;
var settings = restService.GetSettings();
//UCSettings is the "x:name" of the embedded UserControl "UserControlSettings" in this window
this.UCSettings.ExecutionTimes.Clear();
settings.ExecutionTimes.ForEach(x => this.UCSettings.ExecutionTimes.Add(x));
this.UCSettings.TableConfigurationLoader = settings.Timer.Find(x => x.Name == "TableConfigLoader");
}
}
public partial class UserControlSettings : UserControl {
public ObservableCollection<ExecutionTime> ExecutionTimes { get; set; }
public static readonly DependencyProperty TableConfigurationLoaderProperty = DependencyProperty.Register("TableConfigurationLoader", typeof(Setting), typeof(UserControlSettings), new FrameworkPropertyMetadata(default(Setting)));
public Setting TableConfigurationLoader {
get { return (Setting)this.GetValue(TableConfigurationLoaderProperty); }
set { this.SetValue(TableConfigurationLoaderProperty, value); }
}
}
public class Setting {
public string Name { get; set; }
public bool IsEnabled { get; set; }
public int ExecutionTimeId { get; set; }
}
public class ExecutionTime {
public int Id { get; set; }
public string Value { get; set; }
}
In the Code-Designer (UserControlSettings.xaml.cs) these properties are used in some bindings for a ComboBox:
<UserControl x:Class="InsightTool.Gui.UserControlSettings" x:Name="UCSettings">
<ComboBox x:Name="CbConfigLoadingExecutionTime" ItemsSource="{Binding ElementName=UCSettings, Path=ExecutionTimes}" DisplayMemberPath="Value" SelectedValue="{Binding ElementName=UCSettings, Path=TableConfigurationLoader.ExecutionTimeId}" SelectedValuePath="Id"/>
</UserControl>
When I first load in the data with the InitializeService method, everything works fine. The ComboBox is filled with the data of the ObservableCollection and the matching value is selected automatically by the ExecutionTimeId.
When I try to "reinitialize" the service, I call the same method again, but the SelectedValue binding does not work anymore. I checked the values of these properties in the debugger, but they are set correctly in this method again. What am I doing wrong here? Some samples:
Correct display first load:
Incorrect display seconds load:
TableConfigurationLoader is a dependency property. That means a lot of things, but one of them is that when you change the value of TableConfigurationLoader to a different instance of Setting, an event is raised, and this Binding handles that event and updates SelectedValue on the combo box:
SelectedValue="{Binding ElementName=UCSettings, Path=TableConfigurationLoader.ExecutionTimeId}"
However, Setting.ExecutionTimeId isn't a dependency property. It's a regular .NET CLR property, which doesn't notify anybody of anything when its value changes. So if you change the ExecutionTimeId property of the same old Setting that's already in TableConfigurationLoader, nobody knows and nothing happens.
Since Setting is not a control class, you don't particularly need or want its properties to be dependency properties. Instead, you can treat it as a viewmodel. In implementation terms, all a viewmodel really is, is any class that implements INotifyPropertyChanged. With changes to Setting shown below, I think the binding should work as you expect, if I correctly understand your problem. I've changed IsEnabled so it will raise PropertyChanged as well; you may not actually need that, but it's illustrative.
You may need to do the same with your ExecutionTime class.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] String propName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
public class Setting : ViewModelBase
{
public string Name { get; set; }
#region IsEnabled Property
private bool _isEnabled = false;
public bool IsEnabled
{
get { return _isEnabled; }
set
{
if (value != _isEnabled)
{
_isEnabled = value;
OnPropertyChanged();
}
}
}
#endregion IsEnabled Property
#region ExecutionTimeId
private int _executionTimeId = 0;
public int ExecutionTimeId
{
get { return _executionTimeId; }
set
{
if (value != _executionTimeId)
{
_executionTimeId = value;
OnPropertyChanged();
}
}
}
#endregion ExecutionTimeId
}
There are three (ish) mechanisms in WPF for notifying things that properties have changed, and you need to be using one or another somehow if you want things to update correctly:
Dependency properties of dependency objects: For properties of controls
INotifyPropertyChanged: For properties of viewmodels
INotifyCollectionChanged: For collections.
A collection property should also raise INotifyPropertyChanged.PropertyChanged when you assign a new collection instance to it. A given instance of the collection will handle raising its own events when its contents change.
ObservableCollection<T> and ReadOnlyObservableCollection<T> implement INotifyCollectionChanged so you don't have to; it's a big hassle to implement that one properly so you really don't want to go there.
Creating a new instance of Setting before referring to the actual object solved my problem. It seems that the reference to the specific property of Setting is lost, if I just "override" the existing instance of this property:
var settings = restService.GetSettings();
this.UCSettings.ExecutionTimes.Clear();
settings.ExecutionTimes.ForEach(x => this.UCSettings.ExecutionTimes.Add(x));
this.UCSettings.TableConfigurationLoader = new Setting();
this.UCSettings.TableConfigurationLoader = settings.Timer.Find(x => x.Name == "TableConfigLoader");
Try adding UpdateSourceTrigger=PropertyChanged to the binding like this
<UserControl x:Class="InsightTool.Gui.UserControlSettings" x:Name="UCSettings">
<ComboBox x:Name="CbConfigLoadingExecutionTime" ItemsSource="{Binding ElementName=UCSettings, Path=ExecutionTimes}" DisplayMemberPath="Value" SelectedValue="{Binding ElementName=UCSettings, UpdateSourceTrigger=PropertyChanged,Path=TableConfigurationLoader.ExecutionTimeId}" SelectedValuePath="Id"/>

Why does Binding to an instance of my class not work when this.DataContext = this

I have a class that defines a preconfigured socket and all the methods needed to access and control a specific piece of equipment remotely. Part of the class includes an instance of an object that holds the current status of various aspects of the equipment. Each item in the object reports updates using INotifyPropertyUpdate. When I plug it into my test program, all of the methods are called and execute properly, but the only way I seem to be able to get updates of the status to show in the UI is when the DataContext is set to the "Current" object inside the instance of the class. If I set the DataContext to the instance of the class, or to the UI, I stop getting updates in the UI. I would like to be able to use the UI as the DataContext and then bind in the XAML using {Binding Path=InstanceOfMyClass.Current.StatusItemA}
The pertinent parts of the classes in question:
public MyClass : Socket, INotifyPropertyChanged // INotifyPropertyChanged is also used to notify changes in other parts of the class
{
public MyClass : base(//socket configuration info here)
{}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private CurrentStatusObject _current = new CurrentStatusObject();
public CurrentStatusObject Current
{
get { return _current; }
set
{
if (_current != value)
{
_current = value;
NotifyPropertyChanged();
}
}
}
// other methods and properties etc.
}
// this is the Current status object
public class CurrentStatusObject : object, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private string _statusItemA;
public string StatusItemA
{
get { return _statusItemA; }
set
{
if (_statusItemA != value)
{
_statusItemA = value;
NotifyPropertyChanged(); // not necessary to pass property name because of [CallerMemberName]
}
}
}
This works:
c#
this.DataContext = this.InstanceOfMyClass.Current;
XAML
<Label Content="{Binding Path=StatusItemA, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
This does not work, but I want it to:
c#
this.DataContext = this;
XAML
<Label Content="{Binding Path=InstanceOfMyClass.Current.StatusItemA, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
Nor does this:
c#
this.DataContext = this.InstanceOfMyClass;
XAML
<Label Content="{Binding Path=Current.StatusItemA, Mode=OneWay, UpdateSourceTrigger}"/>
I didn't see any answers when searching, but sometimes my research skills fail me. Any help would be appreciated. I enjoy learning new ways of coding. This is my first c# or wpf project. All my projects previous to this have been vb.net in WinForms so I'm at a slight handicap with the learning curve. I would like to learn the correct way to reach my goals for this project, which at this point is simply completing the UI.
The CurrentStatusObject notifies changes internally and that does work. The problem is that the changes are only reflected in the User Interface if I set the DataContext for the UI to that one object. I want to be able to set the DataContext to include a wider scope. I would be happy if I could use the instance of MyClass as the DataContext, but that is not working right now.
The question is Why? and How do I get it to work (using correct practices)?
I presume you have a typo...
public _Current Current = new _Current();
But if so this is a field and not a property. If you change it to this then the binding might work
private _Current _current = new _Current();
public _Current Current
{
get
{
return _current;
}
}
B.T.W: it is not standard to use underscore as part of your class name. Removing it should be all you need
You can only bind to Properties, never fields.
InstanceOfMyClass and Current needs to be declared as properties before you can bind to it (to make DataContext = this work).
As an aside, MVVM dictates that you shouldn't be using your View code-behind as the view model. You should have a separate class for that.

How to auto-notify ListView that its bound property has changed, the MVVM's way?

How to auto-notify ListView that its bound property has changed, the MVVM's way ?
Relevant code-behind:
public partial class MainWindow : Window
{
public DataModel StoreHouse { get; set; }
public ObservableCollection<Units> Devices { get { return StoreHouse.Units; } }
/*
... rest of the code ...
*/
}
XAML binding:
<ListView Name="UnitsListView" ItemsSource="{Binding Devices}">
When I do this:
StoreHouse = newDeserializedStoreHouse
The Units property is no longer valid. Now I can use DependencyProperty, and do this:
StoreHouse = newDeserializedStoreHouse
Units = StoreHouse.Units;
But it's not MVVM-ish... is there a way to do it automatically?
If the Storehouse and the Units are dependent, they should probably be in the same Viewmodel.
You could then just put the ViewModel in the DataContext of the View and bind to both the Storehouse and the Units by speficying the correct binding paths.
Replacing the storehouse is then a change to the ViewModel that could also update the Units or you could set up a completely new ViewModel and assign it to the DataContext.
Use INotifyPropertyChanged for your properties, e.g. like this:
private ObservableCollection<Thing> _things;
public ObservableCollection<Thing> Things
{
get { return _things; }
private set
{
if ( _things != value )
{
_things = value;
OnPropertyChanged();
}
}
}
public PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged( [CallerMemberName] string propertyName = "" )
{
var evt = PropertyChanged;
if ( evt != null)
{
evt( this, new PropertyChangedEventArgs( propertyName ) );
}
}
Do note the use of CallerMemberName on the propertyName parameter; the C# compiler will replace that parameter value with the name of the member in which you call the method, e.g. Things in our example. This is useful to avoid hard-coded strings, which introduce the risk of forgetting to change them if you change the property name. This is available in C# 5 on .NET 4.5; if you use an older C# version, you'll have to use hard-coded strings. (or do magic with expressions, but that's a lot more complex to do)
Many MVVM frameworks have a base class to implement INotifyPropertyChanged easily.

How do I get databound text blocks in a custom control to update?

The setup
So I have a class, ComputerItem, designed to store everything I need to know about a specific computer; these items are stored in an ObservableCollection<ComputerItem>. I then have a custom control ComputerControl, which has (among other things) a few text boxes bound to members of ComputerItem, the bindings made available like so:
<TextBlock Name="tb_computerName"TextWrapping="Wrap" Text="{Binding ElementName=ComputerControl1, Path=computerName}"/>
and in the code behind
public static DependencyProperty computerNameProperty = DependencyProperty.Register("computerName", typeof(string), typeof(ComputerControl), null);
I then create a MultiselectList of ComputerControl objects:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<toolkit:MultiselectList x:Name="lb_computers" IsSelectionEnabledChanged="lb_computers_IsSelectionEnabledChanged"> <!--SelectionChanged="lb_computers_SelectionChanged" >-->
<toolkit:MultiselectList.ItemTemplate>
<DataTemplate>
<StackPanel x:Name="sp">
<local:ComputerControl computerName="{Binding Name}" MACAddress="{Binding DisplayMAC}" playClicked="playClicked_Handler" editClicked="editClicked_Handler"/>
</StackPanel>
</DataTemplate>
</toolkit:MultiselectList.ItemTemplate>
</toolkit:MultiselectList>
</Grid>
and you can see the data bindings in the ComputerControl definition. In the code behind I bind the ObservableCollection to the MultiselectList:
this.lb_computers.ItemsSource = ComputerListMaintainer.GetList();
and all of this (as well as a few things I'm sure I've forgotten to include here) works beautifully to fill the MultiselectList with ComputerControls representing the ComputerItems in the ObservableCollection.
The problem
My issue is that when the underlying ComputerItem changes, the TextBlocks in the corresponding ComputerControl don't update. I've implemented INotifyPropertyChanged in the ComputerItem class:
public class ComputerItem : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string name;
protected virtual void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
public string Name
{
get { return name; }
set { OnPropertyChanged("Name"); name = value; }
}
}
but that didn't solve the problem. I suspect it's something to do with ComputerControl but I have no idea where to start looking; the closest question I've found suggested INotifyPropertyChanged should have been the solution but they weren't using a custom control in that case, just a custom class, if I remember correctly.
What am I missing?
Well your setter is incorrect for starters; also do look into MvvmLight, as it provides a great API for this kind of plumbing work.
public string Name
{
get { return name; }
set
{
if(value != name)
{
name = value;
OnPropertyChanged("Name");
}
}
}
Update:
You shouldn't be setting lb_computers.ItemsSource in your code behind, because that's one time operation and not a binding. It is better to bind to an ObservableCollection of observable objects (aka INotifyPropertyChanged).
Also I'm not sure if you're properly declaring your dependency property, so below you can find a proper setup on how to define a 'bindable' property.
And also with XAML, the architecture of your code matters, to have a sane experience. I highly recommend that you utilize the Mvvm pattern. I find MvvmLight and MEFedMVVM to be great aids in my development. This require a bit of work at the beginning, but it'll be far easier to debug future issues and maintain your code.
If these tips don't help, then I'd have to see your full code.
Declaring a Bindable Property
#region ReportName
public string ReportName
{
get { return (string)GetValue(ReportNameProperty); }
set { SetValue(ReportNameProperty, value); }
}
public static readonly DependencyProperty ReportNameProperty = DependencyProperty.Register("ReportName",
typeof(string), typeof(ExportableGridView), new PropertyMetadata("Report", new PropertyChangedCallback(OnReportNameChanged)));
public static void OnReportNameChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
ExportableGridView control = sender as ExportableGridView;
control.titleTextBlock.Text = e.NewValue as string;
}
#endregion ReportName

ListBox bound to ObservableCollection doesn't update

I am having trouble getting a ListBox binding to work as expected. I'm currently attempting to bind a ListBox to a singleton exposed ObservableCollection of items. The items are a separate class themselves. Currently, I am binding like this:
<Window x:Class="toxySharp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:classes="clr-namespace:MyApplication.Classes"
Title="MainWindow" Height="325" Width="400"
DataContext="{Binding Source={x:Static local:SingletonClass.Instance}}">
<Grid x:Name="LayoutRoot">
<ListBox x:Name="lstMyList" ItemsSource="{Binding Path=Objects, Mode=TwoWay}" DisplayMemberPath="Name" />
</Grid>
</Window>
My singleton is a basic implementation like this:
public class SomeObject : INotifyPropertyChanged
{
private Int32 m_vId;
private String m_vName;
public SomeObject() { }
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public Int32 Id
{
get { return this.m_vId; }
set { this.m_vId = value; NotifyPropertyChanged("Id"); }
}
public String Name
{
get { return this.m_vName; }
set { this.m_vName = value; NotifyPropertyChanged("Name"); }
}
}
public class SingletonClass : INotifyPropertyChanged
{
private static SingletonClass m_vInstance;
private ObservableCollection<SomeObject> m_vObjects;
private SingletonClass()
{
this.m_vObjects = new ObservableCollection<SomeObject>();
for (int x = 0; x < 255; x++)
this.m_vObjects.Add(new SomeObject() { Id = x, Name = String.Format("{0} - new object", x) });
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public static SingletonClass Instance
{
get
{
if (m_vInstance == null)
m_vInstance = new SingletonClass();
return m_vInstance;
}
}
public ObservableCollection<SomeObject> Objects
{
get { return this.m_vObjects; }
set { this.m_vObjects = value; NotifyPropertyChanged("Objects"); }
}
}
Currently, the binding works on startup. The application will bind and properly show the names of each object. For example this is a test app doing the same implementation:
In my main actual application I have async methods being called (Socket stuff BeginConnect, BeginSend, etc.) that use callbacks that can update the collection. (It's a list of players so when certain packets are received the list is updated with their data.)
My problem is when the collection is updated inside one of the async callbacks it doesn't update on the list. The collection data is updated properly, setting a break in the main code anywhere shows the collection being updated but the listbox never updates to reflect the changes. So it just stays saying the same thing no matter what.
Did I overlook something?
I've tried using a CollectionViewSource as well to allow filtering and that has the same problem.
== EDIT ==
I've found the problem which lies in the singleton with how the collection is initialized. Instead of using the internal copy member when initializing the collection, I needed to use the exposed property to allow it to update the UI.
So using the following fixed it:
private SingletonClass()
{
this.Objects = new ObservableCollection<SomeObject>();
for (int x = 0; x < 255; x++)
this.Objects.Add(new SomeObject() { Id = x, Name = String.Format("{0} - new object", x) });
}
However now that the list binding works I want to be able to filter this based on another property inside the object class. (In the example SomeObject). I have a boolean stating if the object is active. Trying to bind to a CollectionViewSource leads me back to the not updating problems. So is there a way to filter this manually and keep the ui updated?
My problem is when the collection is updated inside one of the async
callbacks it doesn't update on the list.
Well thats the problem isnt it! Observable collections are not thread safe. You need to make them that way.
No TwoWay binding mode or UpdateSourceTrigger=PropertyChanged will help in this case as the problem lies with multi threading in your code...
Use this custom implementation of thread safe and faster observable collection for your ease...
Fast performing and thread safe observable collection
As far as INotifyPropertyChanged interface is concerned, its 'PropertyChangedevent is automatically dispatched to the UI thread. So any multithreaded context updating the properties of a class that implementsINotifyPropertyChanged` will update the GUI.
Let me know if this helps,
UpdateSourceTrigger=PropertyChanged is missing
<ListBox x:Name="lstMyList" ItemsSource="{Binding Path=Objects,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" DisplayMemberPath="Name" />

Categories

Resources