I created a small example to demonstrate the issue I'm having.
First my class:
public class DisplayRow : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int?[] values;
private string title;
public string Title
{
get { return title; }
set
{
title = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Title"));
}
}
public int?[] Values
{
get { return values; }
set
{
values = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Values[]"));
}
}
public DisplayRow()
{
Values = new int?[6];
}
}
The problem is the Values property, since it is an array. I'm not sure how to properly call INotifyPropertyChanged when an element in the array gets updated.
Here is my xaml:
<Window x:Class="WpfApplication5.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<ListBox x:Name="MyListBox" Margin="0,0,0,65">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding Path=Title}" />
<TextBlock Text="{Binding Path=Values[0]}" Margin="5,0,0,0" />
<TextBlock Text="{Binding Path=Values[1]}" Margin="5,0,0,0" />
<TextBlock Text="{Binding Path=Values[2]}" Margin="5,0,0,0" />
<TextBlock Text="{Binding Path=Values[3]}" Margin="5,0,0,0" />
<TextBlock Text="{Binding Path=Values[4]}" Margin="5,0,0,0" />
<TextBlock Text="{Binding Path=Values[5]}" Margin="5,0,0,0" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Height="23" Margin="27,0,0,23" Name="button1" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="74" Click="button1_Click">Button</Button>
</Grid>
</Window>
And the code behind:
public partial class Window1 : Window
{
private readonly ObservableCollection<DisplayRow> displayRows = new ObservableCollection<DisplayRow>();
public Window1()
{
InitializeComponent();
displayRows.Add(new DisplayRow {Title = "Item 1", Values = new int?[] {1, 2, 3, 4, 5, 6}});
displayRows.Add(new DisplayRow {Title = "Item 2", Values = new int?[] {7, 8, 9, 10, 11, 12}});
displayRows.Add(new DisplayRow {Title = "Item 3", Values = new int?[] {13, 14, 15, 16, 17, 18}});
MyListBox.ItemsSource = displayRows;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
foreach (DisplayRow row in displayRows)
{
row.Values[0] = 99;
}
}
}
When I click on the button it changes the values of the first row, yet that change is not reflected on the UI. If I change the Title property, the title updates correctly.
Any ideas how I can call INotifyPropertyChanged so that it understands an array element was updated?
The reason your existing code doesn't work is because you're modifying the value within the array, not reassigning entire array itself to the property. Therefore you won't be firing a property change event.
I wouldn't use an array at all, use an ObservableCollection instead which implements INotifyCollectionChanged which the WPF binding infrastructure hooks into to better understand changes within the collection itself.
You could use an ObservableCollection<int?> instead of the array, to do all the plumbing for you.
Another option could be to make an int? container, fill the array with those, and the fire the PropertyChanged event on that int?'s setter.
Related
I have a WPF project where I have created a UserControl for the purpose of making a custom ListViewItem which includes a close button inside. Here is the code for that:
<ListViewItem x:Name="lviTab" Height="36" Background="#232323"
MouseUp="LviTab_MouseUp" MouseEnter="LviTab_MouseEnter">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding TabText}" FontSize="15" />
<ListViewItem x:Name="lviTabClose" Margin="5, 0, 0, 0"
Padding="0, 0, 0, 0" MouseUp="LviTabClose_MouseUp">
<materialDesign:PackIcon Kind="Close" Foreground="White"
VerticalAlignment="Center"
HorizontalAlignment="Center" Width="20" Height="20" />
</ListViewItem>
</StackPanel>
</ListViewItem>
I am adjusting the text inside of each item by binding the text of the TextBlock inside of the UserControl to another class TabData.cs
public class TabData : UIElement
{
public TabData() : base()
{
}
public string TabText { get; set; }
}
In my ListView, I have set the DataTemplate to my UserControl. I have set the ListView ItemSource to a list of TabData objects.
tabs = new List<TabData>()
{
new TabData{TabText = "Tab 1"},
new TabData{TabText = "Tab 2"},
new TabData{TabText = "Tab 3"},
new TabData{TabText = "Tab 4"},
new TabData{TabText = "Tab 5"}
};
lvTabs.ItemsSource = tabs;
When moving the mouse over a ListViewItem, I need IsMouseOver to be true. I've tried inheriting UIElement to TabData but it hasn't worked. I'm quite new to WPF, I would appreciate any help with figuring out how I can retain IsMouseOver property when using a UserControl and setting the ItemSource to items which arn't ListViewItems.
Here's the simplest way to do what you're trying to do. When you populate a ListView with items from a collection, it creates its own ListViewItems. You don't need to create another ListViewItem inside each of its ListViewItems, much less a third one inside your own.
I'm going to dispense with the UserControl and put that XAML straight in a template. The reason for that is that we want the click handler to be in MainWindow.xaml.cs, where it can interact with the main viewmodel. We could fairly easily make that work with the UserControl in a couple of different ways, but I'm keeping this as simple as I can. Ideally you would use a Command for that, but that's one particular case where a little impurity in your MVVM won't ruin you. And for a case where the item UI is as simple as one textblock and one button, a UserControl is more than you need.
First, MainWindow.xaml
<Window.Resources>
<DataTemplate x:Key="TabListViewItemTemplate">
<DockPanel LastChildFill="False">
<TextBlock Text="{Binding TabText}" DockPanel.Dock="Left" />
<Button x:Name="TabCloseButton" Click="TabCloseButton_Click" DockPanel.Dock="Right" >
<Button.Template>
<ControlTemplate TargetType="Button">
<materialDesign:PackIcon
Kind="Close" Foreground="White"
VerticalAlignment="Center" HorizontalAlignment="Center"
Width="20" Height="20" />
</ControlTemplate>
</Button.Template>
</Button>
</DockPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ListView
ItemsSource="{Binding TabItems}"
ItemTemplate="{StaticResource TabListViewItemTemplate}"
HorizontalContentAlignment="Stretch"
/>
</Grid>
MainWindow.xaml.cs
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel
{
TabItems = {
new TabData { TabText = "Fred" },
new TabData { TabText = "Ginger" },
new TabData { TabText = "Herman" },
}
};
}
private void TabCloseButton_Click(object sender, RoutedEventArgs e)
{
if ((sender as FrameworkElement).DataContext is TabData tabData)
{
MessageBox.Show($"TabCloseButton_Click() {tabData.TabText}");
}
}
The TabData class. Don't inherit from UIElement. It's not an element in the user interface, it's just a plain C# class. If you were going to let the user edit TabText, you would make it a viewmodel that implements INotifyPropertyChanged.
public class TabData
{
public string TabText { get; set; }
}
The main viewmodel. Nothing we're doing here actually requires INotifyPropertyChanged on any of your classes, but you'll need it if you turn this into anything useful, so we'll include it.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
public class MainViewModel : ViewModelBase
{
public ObservableCollection<TabData> TabItems { get; } = new ObservableCollection<TabData>();
}
As seen in the picture i have a ComboBox which is showing me all the Events that i have in my Database. This could be a birthday party for example. The Listview is showing me the participants. This is all working perfect. But, when i add a new Event in the running application, using the textboxes nd the button "Toevoegen" Which translates to "Add" My Combobox is not showing the new event. When i restart the program it does show it.
I figured out that it has something to do with the property changed. But how do i use this when i add my items to an instance of eventmanager.events.Add(item)?
Xaml
<Window x:Class="Databinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Databinding"
mc:Ignorable="d"
Title="Events" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ComboBox x:Name="cbEvents1" ItemsSource="{Binding events, Mode=TwoWay}" SelectedItem="{Binding currentEvent}" SelectedValuePath="Content" Margin="10,10,31.667,381.667">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ListView Grid.Column="1" ItemsSource="{Binding participants}">
<ListView.View>
<GridView>
<GridViewColumn Header="First Name" DisplayMemberBinding="{Binding firstName}"/>
<GridViewColumn Header="Last Name" DisplayMemberBinding="{Binding lastName}"/>
</GridView>
</ListView.View>
</ListView>
<Label Content="Nieuw evenement " HorizontalAlignment="Left" Margin="10,53,0,0" VerticalAlignment="Top"/>
<Label Content="Naam:" HorizontalAlignment="Left" Margin="44,80,0,0" VerticalAlignment="Top" RenderTransformOrigin="1.083,0.564"/>
<TextBox x:Name="tbNaamEv" HorizontalAlignment="Left" Height="23" Margin="93,79,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="272"/>
<Label x:Name="lblOmschrijving1" Content="Omschrijving:" HorizontalAlignment="Left" Margin="10,115,0,0" VerticalAlignment="Top"/>
<TextBox x:Name="TbOmschrijvingEV" HorizontalAlignment="Left" Height="23" Margin="93,119,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="272"/>
<Label Content="Opmerking:" HorizontalAlignment="Left" Margin="21,151,0,0" VerticalAlignment="Top"/>
<Label Content="Datum:" HorizontalAlignment="Left" Margin="46,186,0,0" VerticalAlignment="Top"/>
<TextBox x:Name="tbOpmerkingEv" HorizontalAlignment="Left" Height="23" Margin="93,154,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="205"/>
<Button x:Name="btnAdd1" Content="Toevoegen" HorizontalAlignment="Left" Margin="207,234,0,0" VerticalAlignment="Top" Width="158" Height="24" Click="btnAdd_Click"/>
<DatePicker x:Name="DPevenement" HorizontalAlignment="Left" Margin="93,189,0,0" VerticalAlignment="Top" Width="205" FirstDayOfWeek="Monday" IsDropDownOpen="True"/>
<Label Content="Evenement informatie" HorizontalAlignment="Left" Margin="6,270,0,0" VerticalAlignment="Top"/>
<Label Content="Omschrijving:" HorizontalAlignment="Left" Margin="6,301,0,0" VerticalAlignment="Top"/>
<Label x:Name="lblOmschrijvingEv" Content="{Binding omschrijving}" HorizontalAlignment="Left" Margin="93,301,0,0" VerticalAlignment="Top" Width="294" Height="26"/>
<Label Content="Opmerking:" HorizontalAlignment="Left" Margin="16,344,0,0" VerticalAlignment="Top"/>
<Label x:Name="lblOpmerkingEv" Content="{Binding opmerking}" HorizontalAlignment="Left" Margin="93,344,0,0" VerticalAlignment="Top" Width="294" Height="26"/>
<Label Content="Datum:" HorizontalAlignment="Left" Margin="40,385,0,0" VerticalAlignment="Top"/>
<Label x:Name="lblDatumEv" Content="{Binding Datum}" HorizontalAlignment="Left" Margin="93,385,0,0" VerticalAlignment="Top" Width="294" Height="26"/>
</Grid>
</Window>
Eventmanager Class
public class EventManager : INotifyPropertyChanged
{
public List<Event> events { get; set; }
public List<People> peoples { get; set; }
//this is the current event that correspond to the selected event in your combobox
private Event _currentEvent;
public Event currentEvent
{
get
{
return _currentEvent;
}
set
{
if (_currentEvent != value)
{
_currentEvent = value;
//when you change the selected event, you have to update the list of participants
OnPropertyChanged("participants");
}
}
}
public List<People> participants
{
get
{
//Here is the code to retrieve the people that registered to the selected event
return peoples.Where(p => p.registeredEvents.Contains(currentEvent)).ToList<People>();
}
}
public EventManager()
{
events = new List<Event>();
peoples = new List<People>();
}
//The following lines are specific to WPF and DataBinding
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
People Class
public class People : INotifyPropertyChanged
{
public string firstName { get; set; }
public string lastName { get; set; }
public List<Event> registeredEvents { get; set; }
public People()
{
registeredEvents = new List<Event>();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
public class Event : INotifyPropertyChanged
{
public string name { get; set; }
public string omschrijving { get; set; }
public string opmerking { get; set; }
public DateTime Datum { get; set; }
//The following lines are specific to WPF and DataBinding
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
This is how i add my events and participants:
EventManager eventManager = new EventManager();
DBConnect connect = new DBConnect();
public Event Selected;
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
connect.EvToevoegen(tbNaamEv.Text, TbOmschrijvingEV.Text, tbOpmerkingEv.Text, Convert.ToDateTime(DPevenement.SelectedDate));
Selected = new Event()
{
name = tbNaamEv.Text
};
eventManager.events.Add(Selected);
}
}
The new Event won't show in my ComboBox?
Lists don't respond to the PropertyChanged events like you expect because the List itself likely never changed. Its contents did.
For ItemsSource to respond to a collection changing, that collection must implement the INotifyCollectionChanged interface. C# provides us with a collection that already implements that interface, thankfully.
If you use ObservableCollection<T> you can get this functionality immediately.
Try replacing:
public List<Event> events { get; set; }
With:
public ObservableCollection<Event> events { get; set; }
If you are unable to change the type of this collection for whatever reason, you are going to need to wrap the list somehow.
The most simple way to do this is by using the copy constructor exposed by observable collection:
new ObservableCollection<T>(IEnumerable<T>)
Well first of all your People class is implementing INotifyPropertyChanged but none of the properties are actually raising OnPropertyChanged (unless you're using a framework like Caliburn or something to implement it automatically).
To answer your question though, your registeredEvents property needs to implement INotifyCollectionChanged e.g. be of type ObservableCollection<Event>. This creates a few potential problems though when you're serializing to and from a database, because if your database layer returns a List, and you convert it to an ObservableCollection, then when you go to save it again the database will think the entire list has changed and re-serialize the whole thing back out again irrespective of whether or not anything has actually changed. Obviously this will result in a serious performance hit.
How you best resolve this will depend on other parts of your application. You may choose to keep your ObservableCollection separate, then compare it to the original list when the user is finished and update the original list all in one go. Many ORMs allow you to control the types of data structures they create, in which case you can make them create ObservableCollections for all lists, and this problem doesn't exist in the first place. Your view model layer may choose to keep both lists in memory and add/remove elements from both of them at runtime, once for the benefit of the database and the other for the view. Alternatively, if your lists are small, you could simply continue to use a List<Element>, raise OnPropertyChanged("registeredEvents") whenever you make any changes to the list and cop the performance hit of having all the list GUI elements re-create themselves.
Problem
I am trying to bind a ComboBox's SelectedItem to a custom class but this does not update when the property is changed.INotifyPropertyChanged is implemented.
The DataContext
The DataContext is a custom class which contains many properties, but an extract of this is below. You can see it implements INotifyPropertyChanged and this called when the two properties are changed.
public class BctsChange : INotifyPropertyChanged
{
#region declarations
private byContact _Engineer;
public byContact Engineer
{
get { return _Engineer; }
set
{
_Engineer = value;
NotifyPropertyChanged("Engineer");
OnEngineerChanged();
}
}
private BctsSvc.DOSets _LeadingSet;
public BctsSvc.DOSets LeadingSet
{
get { return _LeadingSet; }
set { _LeadingSet = value; NotifyPropertyChanged("LeadingSet"); }
}
#endregion
#region INotify
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
public BctsChange()
{
Engineer = new byContact(Environment.UserName);
}
private void OnEngineerChanged()
{
if (Engineer != null)
{
BctsSvc.DOSets leadSet = GetLeadingSetFromDeptCode(Engineer.DeptCode);
if (leadSet == null) return;
LeadingSet = leadSet;
}
}
private static BctsSvc.DOSets GetLeadingSetFromDeptCode(string DeptCode)
{
BctsSvc.BctsServiceSoapClient svc = new BctsSvc.BctsServiceSoapClient();
BctsSvc.DOSets setX = svc.GetSetFromDeptCode(DeptCode);
return setX;
}
}
The Window XAML
I have several controls on the window, but to keep the code simple I believe the following extract will suffice.
<Window x:Class="MyNamespace.wdSubmit"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:MyNamespace"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
x:Name="ucReqForm"
Title="wdSubmit" >
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
<GroupBox Header="Engineer Details" Name="grpOwnerDetails" >
<StackPanel Orientation="Vertical">
<Grid VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="35"/>
</Grid.ColumnDefinitions>
<Label Content="{Binding Engineer.FullName, FallbackValue='Please select an engineer by clicking →', Mode=OneWay}" Margin="5,0" IsEnabled="True" FontStyle="Italic" />
<Button Content="{StaticResource icoSearch}" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Grid.Column="1" Height="23" Name="btnSelectEngineer" Margin="0,0,5,0" HorizontalAlignment="Stretch" ToolTip="Search for an engineer responsible" Click="btnSelectEngineer_Click" />
</Grid>
<ComboBox Height="23" x:Name="ddSet2" Margin="5,0" ItemsSource="{Binding LeadingSets, Mode=OneWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" SelectedItem="{Binding LeadingSet, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,NotifyOnTargetUpdated=True}" >
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding SetName}" ToolTip="{Binding HelpInfo}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<my:LabelledDropdown Height="23" x:Name="ddSet" Margin="5,0" ItemsSource="{Binding LeadingSets, Mode=OneWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" SelectedItem="{Binding LeadingSet, Mode=TwoWay,NotifyOnTargetUpdated=True,NotifyOnSourceUpdated=True}" Label="e.g. BodyHardware">
<my:LabelledDropdown.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding SetName}" ToolTip="{Binding HelpInfo}"/>
</DataTemplate>
</my:LabelledDropdown.ItemTemplate>
</my:LabelledDropdown>
</StackPanel>
</GroupBox>
</StackPanel>
</Window>
The above extract contains:
A Label that contains a contact's name, and a button to search for a contact, bound to the FullName of the Engineer
A ComboBox that contains departments within the company, bound to an ObservableCollection<DOSets>, which contains a list of departments
Two ComboBoxes, one which is a custom one and the other which is temporary to ensure the bug is not within the control. These are Databound to LeadingSet
Window Code Behind
In the code behind I set the DataContext to CurrentChange. When the user wants to select a different Engineer then this will update the selected department for the engineer in CurrentChange.
When the user changes the engineer, the data binding for the engineer is updated, but the selected department (Leading Set) isn't.
//Usings here
namespace MyNamespace
{
public partial class wdSubmit : Window, INotifyPropertyChanged
{
private BctsSvc.BctsServiceSoapClient svc;
private BctsChange _CurrentChange;
public BctsChange CurrentChange
{
get { return _CurrentChange; }
set { _CurrentChange = value; OnPropertyChanged("CurrentChange"); }
}
private List<BctsSvc.DOSets> _LeadingSets;
public List<BctsSvc.DOSets> LeadingSets
{
get
{
return _LeadingSets;
}
}
public wdSubmit()
{
InitializeComponent();
svc = new BctsSvc.BctsServiceSoapClient();
_LeadingSets = svc.GetLeadSets().ToList();
OnPropertyChanged("LeadingSets");
this._CurrentChange = new BctsChange();
this.DataContext = CurrentChange;
CurrentChange.PropertyChanged += new PropertyChangedEventHandler(CurrentChange_PropertyChanged);
}
void CurrentChange_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged("CurrentChange");
OnPropertyChanged(e.PropertyName);
}
private void btnSelectEngineer_Click(object sender, RoutedEventArgs e)
{
byContact newContact = new frmSearchEngineer().ShowSearch();
if (newContact != null)
{
CurrentChange.Engineer = newContact;
PropertyChanged(CurrentChange, new PropertyChangedEventArgs("LeadingSet"));
PropertyChanged(CurrentChange.LeadingSet, new PropertyChangedEventArgs("LeadingSet"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(CurrentChange, new PropertyChangedEventArgs(propertyName));
}
}
}
I've realised the problem may be due to the LeadingSet, returned when the engineer is changed, being a different instance to that in the ObservableCollection.
I am trying to get data updates to work both into and out of XAML. I.e, when I make a change in the XAML TextBox the C# will receive the new value and when I change the C# (simulated by clicking a button), both the XAML TextBox changes. I have got this to work, however if I make a change to the XAML TextBox it doenst update the XAML ItemsList. Any ideas how I can get this working?
MainWindow.xaml...
<Window x:Class="MySimpleProgram.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="725"
>
<StackPanel Name="StackPanel1" Orientation="Horizontal">
<TextBox Name="TextBox2" Text="{Binding Path=FirstName, Mode=TwoWay}" Height="23"/>
<Button Name="Button1" Content="Change C# obj people[0]" Width="175" Height="20" Click="Button1_Click" />
<ListBox Name="listPeople" DisplayMemberPath="FirstName"/>
</StackPanel>
</Window>
MainWindow.xaml.cs
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private String _FirstName;
public string FirstName
{
get { return _FirstName; }
set
{
_FirstName = value;
if (PropertyChanged != null)
PropertyChanged(
this, new PropertyChangedEventArgs("FirstName"));
}
}
public int Age { get; set; }
}
public partial class MainWindow : Window
{
public Person[] people;
public MainWindow()
{
InitializeComponent();
people = new Person[]{
new Person{ FirstName = "Shirley", Age = 22 },
new Person{ FirstName = "Roy", Age = 29 },
new Person{ FirstName = "Manuel", Age = 34 } };
StackPanel1.DataContext = people[0];
listPeople.ItemsSource = people;
}
private void Button1_Click(object sender, RoutedEventArgs e)
{
people[0].FirstName += "y";
}
}
Set UpdateSourceTrigger to PropertyChanged on your TextBox binding.
Default value is LostFocus i.e. source updates on lost focus but in case you want updation when you typing in set it to PropertyChanged. and you will see update in ListBox.
<TextBox Name="TextBox2"
Text="{Binding Path=FirstName, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
Height="23"/>
Hi :) I’m just learning C# and WPF and I need to write a tool that:
Loads a data file (string Key, int Value)
Binds the data to a WPF UI for edits
Saves a new data file
My thought was a dictionary would be best.
The data is only loaded once from file, and changes are only made with the WPF controls.
I’ve tried many things but I still keep hitting road blocks when I bind data to the controls.
I’ve been working with a simplified version of the tool – below.
The data binds to the WPF control – but there is no change event to update the dictionary.
I haven’t found a good example to follow.
Could someone explain how to get the dictionary to update?
And is the strategy the right one? - using a dictionary -using DataContext.
If you'd like to see the full project and UI - there is a link at the bottom.
I've been working many-many days...with progress but I'm way too slow ;)
Cheers
Danny
MainWindow.xaml
<Window x:Class="configGen.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="200" Width="150">
<StackPanel Margin="20" Width="80">
<TextBox Text="{Binding [item1]}" />
<TextBlock Text="{Binding [item1]}" />
<TextBox Text="{Binding [item2]}" />
<TextBlock Text="{Binding [item2]}" />
<TextBox Text="{Binding [item3]}" />
<TextBlock Text="{Binding [item3]}" />
<Slider Value="{Binding [item4]}" Minimum="0" Maximum="256" />
<TextBlock Text="{Binding [item4]}" />
</StackPanel>
MainWindow.xaml.cs
namespace configGen
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
dataClass record = new dataClass();
DataContext = record.generate();
}
}
public class dataClass
{
public Dictionary<string, int> generate()
{
Dictionary<string, int> _data = new Dictionary<string, int>();
_data.Add("item1", 100);
_data.Add("item2", 120);
_data.Add("item3", 140);
_data.Add("item4", 160);
return _data;
}
}
}
Link to full project...
http://www.baytower.ca/btsRV7config.zip
Thanks for all the great feedback everyone!!
I will set back to work :)
Instead of using a Dictionary as your DataContext I'd create a custom object like MainViewModel. Give it properties that correspond to item1, item2, etc, except give them appropriate names. Then use <TextBox Text="{Binding MyPropertyName}" />. To handle updates, you can either set your DataContext to a new MainViewModel object or you can set up your class to broadcast property changes. You can do that either through INotifyPropertyChanged on the class or with dependency properties.
At least that's what it seems like you're trying to accomplish. If you're going for displaying an arbitrary number of controls you'd need something different.
A dictionary is definitly not a convenient way to do a two way data binding in WPF. It seems an ObservableCollection is more suited to your requirements.
Something like:
public class ItemsList : ObservableCollection<Item>
{
public ItemsList() : base()
{
Add(new Item("item 1", 100));
Add(new Item("item 2", 120));
Add(new Item("item 3", 140));
Add(new Item("item 4", 160));
}
}
Item is a simple class with a name and a value properties. I have ommitted it here because it is self explanatory.
The advantage here is that you can bind to a dynamic number of items not only the ones declared imperatively.
Once you bind you datacontext to it, you get the automatic property notification for two way databinding.
Your XAML will have to change to accomodate binding to a collection of course. Maybe an ItemsControl that takes that collection as its ItemsSource.
Here is an example of how I would do it.
Main Class (Code Behind)
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private List<MyObject> _myObjects;
public List<MyObject> MyObjects
{
get { return _myObjects; }
set
{
_myObjects = value;
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("MyObjects"));
}
}
}
public MainWindow()
{
MyObjects = new List<MyObject>();
// Add 20 records for sample data
for (int i = 0; i < 20; i++)
{
MyObject o = new MyObject();
o.Label = string.Format("Key{0}", i);
o.MyValue = string.Format("Value{0}", i);
MyObjects.Add(o);
}
InitializeComponent();
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
}
Secondary Class
public class MyObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _label;
public string Label
{
get { return _label; }
set
{
_label = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Label"));
}
}
}
private string _myValue;
public string MyValue
{
get
{
return _myValue;
}
set
{
_myValue = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("MyValue"));
}
}
}
}
XAML File
<Window x:Class="WpfApplication4.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="listboxstyle">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Path=Label}" />
<TextBox Grid.Column="1" Text="{Binding Path=MyValue}" />
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox
ItemsSource="{Binding Path=MyObjects}"
ItemTemplate="{StaticResource listboxstyle}"
/>
</Grid>
</Window>
I've tried tsell's example, using his class in a list. The list is just a convenient way to generate and manage a fixed number of elements. The Item1 WPF control binds to the Item1 object and its value. The object is found by its index number. The binding and dataContext in this case is simple enough for me to use (as a beginner). It works, but I'm not sure it's exactly an elegant way to do it.
public MainWindow()
{
MyObjects = new List<MyObject>();
MyObject item1 = new MyObject();
item1.MyValue = string.Format("100");
MyObjects.Add(item1);
MyObject item2 = new MyObject();
item2.MyValue = string.Format("120");
MyObjects.Add(item2);
MyObject item3 = new MyObject();
item3.MyValue = string.Format("140");
MyObjects.Add(item3);
MyObject item4 = new MyObject();
item4.MyValue = string.Format("160");
MyObjects.Add(item4);
InitializeComponent();
DataContext = this;
}
xaml
<Window x:Class="WpfApplication4.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel Margin="20" Width="80">
<TextBox Text="{Binding MyObjects[0].MyValue}" />
<TextBlock Text="{Binding MyObjects[0].MyValue}" />
<TextBox Text="{Binding MyObjects[1].MyValue}" />
<TextBlock Text="{Binding MyObjects[1].MyValue}" />
<TextBox Text="{Binding MyObjects[2].MyValue}" />
<TextBlock Text="{Binding MyObjects[2].MyValue}" />
<Slider Value="{Binding MyObjects[3].MyValue}" Minimum="0" Maximum="256" />
<TextBlock Text="{Binding MyObjects[3].MyValue}" />
</StackPanel>
</Window>
BTW, I will change to int for MyValues..they are all int numbers. For now it is a string.