WPF TabControl Header and Content not updating? - c#

Setup: I have a tabcontrol with 0...n tabs, and each tab is bound to a class:
class MyTabItem
{
string Text {get; set;}
int ID {get; set;}
ISet<MyTabContent> Contents {get; set;}
}
Class MyTabContent
{
int ID {get; set;}
string Subtext {get; set;}
}
Each tabitem class has many tabcontent classes in it's set. (The whole thing gets fetched via NHibernate).
I've tried a lot of things to bind the content of MyTabItem to the header of each tabcontrol item, and the content of MyTabContent to a datagrid in the content of each tabcontrol item.
I can display all the content in the tabs, but whenever I change properties in the bound classes, the UI does not update. I've tried to call InvalidateVisual, tried to Dispatch a Render event, tried to UpdateTarget and UpdateSource on the bindings (those last 2 threw exceptions). I've implemented INotifyPropertyChanged in my viewmodel, and even tried manually using OnPropertyChanged("MyTabItem") to no avail.
I really don't understand why my tabcontrol contents won't update when I change properties in the bound classes. I've tried 2 different binding strategies, either works in displaying the content, but neither updates when the content changes.
My XAML setup is:
<TabControl>
<TabControl.ItemTemplate>
<DataTemplate DataType="models:MyTabItem">
<TextBlock Text="{Binding Text}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="models:MyTabItem">
<DataGrid ItemsSource="{Binding Contents}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=Subtext}" />
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
With that XAML setup, I simply added Items to the tabcontrol with tabcontrol.Items.Add(new MyTabItem).
Then I tried another XAML setup where I bound the tabcontrol.Itemsource to an ObservableCollection in the code-behind. Again, the tab content did not update if the bound properties change :(
I also tried making a CurrentItem property in the ViewModel, and used that as a Window.Resource, and then bound the Tabcontrol contents to
{Binding Source={StaticResource CurrentItem}, Path=Text}
And whenever I changed tabs I would update the CurrentItem in the viewmodel, but with that it also didn't update changes.
I'm pretty much out of ideas :(

You need to implement INotifyPropertyChanged
Keep in mind there's a new attribute in .NET 4.5 that simplifies the task, take a look here
Here's a sample, apply that to both your classes, the list will need to become ObservableCollection:
private ObservableCollection<MyTabContent> _contents = new ObservableCollection<MyTabContent>();
public ObservableCollection<MyTabContent> Contents { get { return _contents; } }
-
public class MyTabContent : INotifyPropertyChanged
{
private int _id;
int ID {
get{ return _id; }
set{ _id = value; OnPropertyChanged(); }
}
private string _subText;
public string Subtext {
get{ return _subText; }
set{ _subText= value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged!= null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

Related

How to bind a datatype to a control in WinUI 3?

I have a datatype (model) I would like to display the data for in my UI by showing several properties using data binding.. It works in a GridView or ListView, but how do I do this when I only want a single model bound instead of a collection?
To do this with a collection, the following works in a ListView:
<ListView x:Name="MyListView"
ItemsSource="{x:Bind Shapes, Mode=OneWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:Shape">
<StackPanel>
<TextBlock Text="{x:Bind Name}"></TextBlock>
<TextBlock Text="{x:Bind NumberOfSides}"></TextBlock>
<TextBlock Text="{x:Bind Color}"></TextBlock>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
On a page with a ObservableCollection of type Shape called Shapes:
public sealed partial class MyPage : Page
{
// ...
public ObservableCollection<Shape> Shapes { get; set; }
// ...
}
With the following model Shape:
public class Shape
{
public string Name { get; set; }
public string NumberOfSides { get; set; }
public string Color { get; set; }
}
I want to do something like this, but this does not work:
<Grid>
<StackPanel>
<TextBlock Text="{x:Bind Name}"></TextBlock>
<TextBlock Text="{x:Bind NumberOfSides}"></TextBlock>
<TextBlock Text="{x:Bind Color}"></TextBlock>
</StackPanel>
</Grid>
The data-binding is actually being done to the ListView, and the DataTemplate is simply declaring the layout to display the bound model with.
To accomplish this with a single bound item instead of a collection, you need to use a control that still has a template property. This is where the ContentControl comes in (Microsoft's official documentation). The ContentControl has a ContentTemplate property, which can contain a DataTemplate the same way a ListView or GridView can! You can then set the Content property of the ContentControl in the C# code, or bind to it (the same way you would bind to an ItemsSource property of a ListView or GridView, only with a single item instead of a collection).
The Simple Way
The following example works (Note that the DataTemplate and all of it's children are identical to how they would appear in a ListView or GridView):
<ContentControl x:Name="MyContentControl">
<ContentControl.ContentTemplate>
<DataTemplate x:DataType="models:Shape">
<StackPanel>
<TextBlock Text="{x:Bind Name}"></TextBlock>
<TextBlock Text="{x:Bind NumberOfSides}"></TextBlock>
<TextBlock Text="{x:Bind Color}"></TextBlock>
</StackPanel>
</DataTemplate>
<ContentControl.ContentTemplate>
</ContentControl>
Then in your C# code:
public sealed partial class MyPage : Page
{
// ...
public void SetShape(Shape shape)
{
this.MyContentControl.Content = shape;
}
// ...
}
The FULL Data Binding Way
You can also use data binding to bind to the shape property, but this will require some more work. Start by adding the binding to the ContentControl as follows:
<ContentControl x:Name="MyContentControl"
Content="{x:Bind MyShape}">
<ContentControl.ContentTemplate>
<!-- Contents all the same as before -->
<ContentControl.ContentTemplate>
</ContentControl>
And add the MyShape property to bind to on MyPage:
public sealed partial class MyPage : Page
{
// ...
public Shape MyShape { get; set; }
// ...
}
As is, this will not work. It may work when you set it initially, but if you change MyShape, the bound UI will not update.
Notice that if you were using ObservableCollection (such as in the ListView example), you can get the UI to update when you call Add() or Remove() functions of the ObservableCollection, but not when you change the ObservableCollection reference itself. The reason is that the ObservableCollection implements INotifyPropertyChanged which is what tells the bindings to update when you change the set of items in the collection. The following will not automatically work:
public sealed partial class MyPage : Page
{
// ...
public Shape MyShape { get; set; }
// ...
public void UpdateShape(Shape newShape)
{
this.MyShape = newShape;
}
}
To get this to work, you need to implement INotifyPropertyChanged on MyPage. This requires three steps (which may sound intimidating, but work the same way for any property):
Implement the interface INotifyPropertyChanged.
Add the PropertyChanged event.
Modify the MyShape setter to raise the PropertyChanged event.
Implement the interface INotifyPropertyChanged.
public sealed partial class MyPage : Page, INotifyPropertyChanged
{
// ...
}
Add the PropertyChanged event.
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raise the PropertChanged event for the given property name.
/// </summary>
/// <param name="name">Name of the property changed.</param>
public void RaisePropertyChanged(string name)
{
// Ensure a handler is listening for the event.
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
Modify the MyShape setter to raise the PropertyChanged event.
private Shape myShape;
public Shape MyShape
{
get => this.myShape;
set
{
this.myShape = value;
this.RaisePropertyChanged("MyShape");
}
}
Your final C# code will look like this:
public sealed partial class MyPage : Page, INotifyPropertyChanged
{
// ...
private Shape myShape;
public Shape MyShape
{
get => this.myShape;
set
{
this.myShape = value;
this.RaisePropertyChanged("MyShape");
}
}
// ...
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raise the PropertChanged event for the given property name.
/// </summary>
/// <param name="name">Name of the property changed.</param>
public void RaisePropertyChanged(string name)
{
// Ensure a handler is listening for the event.
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
// ...
public void UpdateShape(Shape newShape)
{
this.MyShape = newShape;
}
}
NOW your ContentControl will work as expected with the different BindingMode values (OneTime, OneWay, and TwoWay).
If you want your bound controls WITHIN the ContentControl to update when you change a property of the shape, such as have the <TextBlock Text="{x:Bind Name}"> update when you do:
this.MyShape.Name = "A New Name";
You can similarly implement INotifyPropertyChanged on your Shape class itself with the same basic steps. This is the same whether you are using a ContentControl, GridView, ListView, or any other data-bound control. Basically, each layer you want to be able to update the properties of, and have a data bound UI update, you need to do this. This also needs to be done regardless of which of the two ways you used from this answer. You can refer to my answer here for details on this.

Combobox binding issue

My ComboBox does not get populated with data.
Class Employee set to public, has variables such as:
public int EmployeeID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
Code on UserControl:
public IEnumerable<csEmployee> employeeList;
public ObservableCollection<csEmployee> _employeeSorted { get; set; }
public ucAddClient()
{
InitializeComponent();
//Establish connection
var GetMyData = new DataAccess();
//Get data by procedure
employeeList = GetMyDataPV.ExecuteStoredProc<csEmployee>("procedure", new {KeyDate = Key_to_extract});
employeeList = employeeList.Where(record => record.EmployeeLevelID > 300);
_employeeSorted = new ObservableCollection<csEmployee>(employeeList.Where(record => record != null));
}
And WPF:
<ComboBox x:Name="cbAddManager"
Foreground="#FF4D648B"
FontSize="12"
IsEditable="True"
ItemsSource="{Binding _employeeSorted}"
DisplayMemberPath="FirstName"
PreviewKeyDown="cbAddManager_PreviewKeyDown"
Width="200">
<!--<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Width ="50" Text="{Binding LastName}"/>
<TextBlock Text=", "/>
<TextBlock Width ="50" Text="{Binding FirstName}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>-->
</ComboBox>
Do you have any idea, why ComboBoxis not populated? When I do this in code (I add it in user control class) it gets data needed.
Im not sure if Im binding it correctly?
That is because you assign a new instance of a collection to your _employeeSorted property after InitializeComponent. At that time, the binding is already set up and does not get notified that you have updated the property from null, because you do not implement INotifyPropertyChanged.
There are multiple ways to solve the issue:
Initialize the collection before InitializeComponent and work on this same collection if you intend to change it, using Clear and Add instead of creating a new instance on changes.
Implement the INotifyPropertyChanged interface and use it to notify changes to your property so that the bindings are updated the the changes are applied in the user interface, e.g.:
public partial class MyUserControl : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<csEmployee> _employeeSortedField;
public ObservableCollection<csEmployee> _employeeSorted
{
get => _employeeSortedField;
set
{
if (_employeeSortedField == value)
return;
_employeeSortedField = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Expose a depenedency property for the collection instead and bind it to a collection in your view model that is passed as data context of the UserControl, thus moving the data access out it and separating the view from the business logic and data (recommended, see below MVVM).
Another issue might be that you do not set your data context to the UserControl itself in XAML (which is not recommened by the way, although it might solve your issue). In this case, the binding is unable to resolve the property at runtime (a binding error will be shown in the output window).
<UserControl x:Class="YourProject.YourControl"
...
DataContext="{Binding RelativeSource={RelativeSource Self}}">
As a note, it seems that you mix your business logic with your UserControl (view). Leverage the MVVM design pattern to create view models and seprate both concerns instead. Furthermore, if you set the data context of your UserControl to itself, you break data context inheritance.

C# / WPF Bind in TabControl ItemsSource to property in code behind of List item

In my ViewModel I have a BindingList which holds my Views that should be displayed as tabs in my TabControl. The text for the tab is defined in the code behind of the view. I also defined a simple test class to test the binding which works perfectly. Only the binding to the code behind property does not work.
Xaml code for my TabControl:
<TabControl ItemsSource="{Binding TabControlContentList}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=DisplayName, FallbackValue=FallbackValue}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
The BindingList which it is bound to:
void RaisePropertyChanged([CallerMemberName] string propertyName = "")
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public BindingList<object> TabControlContentList
{
get => tabControlContentList;
set
{
tabControlContentList = value;
RaisePropertyChanged();
}
}
private BindingList<object> tabControlContentList = new BindingList<object>();
Test class:
class ControlTest
{
public string DisplayName { get; private set; } = "";
public ControlTest(string name) => DisplayName = name;
}
Xaml code behind of AnalysisView.xaml.cs:
public partial class AnalysisView
{
public string DisplayName { get; private set; } = "Analysis";
public AnalysisView()
{
InitializeComponent();
}
}
In my ViewModel I have the following code:
public AnalysisView analysisView = new AnalysisView();
TabControlContentList.Clear();
TabControlContentList.Add(new ControlTest("Menu 1"));
TabControlContentList.Add(new ControlTest("Menu 2"));
TabControlContentList.Add(analysisView);
TabControlContentList.Add(new ControlTest("Menu 3"));
TabControlContentList.Add(new ControlTest("Menu 4"));
My TabControl then shows five tabs. First two have the text "Menu 1" and "Menu 2" on them, the third one reads "FallbackValue", followed by "Menu 3" and "Menu 4".
I have no idea anymore why the binding on the property in code behind in AnalysisView.xaml.cs does not work. Is this maybe a general Wpf thing?
Before I present my answer, I highly recommend that you download and learn how to use Snoop for WPF, or use the Visual Studio built-in XAML runtime inspection tools. These allow you to look at bindings, data context, etc.
The main reason why your code doesn't work, is that a UserControl doesn't have a DataContext by default.
So, updating your AnalysisView like so will give it a DataContext pointing to itself:
public AnalysisView()
{
InitializeComponent();
DataContext = this;
}
However, the DataContext in a UserControl won't flow into your DataTemplate like your other objects. To make it work, you need to change your binding like so:
<DataTemplate>
<TextBlock Text="{Binding Path=DataContext.DisplayName, FallbackValue=ERROR!, RelativeSource={RelativeSource AncestorType={x:Type TabItem}}}" />
</DataTemplate>
Working with UserControls like this feels like a hack. So you might want to look for ways to design whatever app you're building a little differently.
Also, the code in your sample is not using INotifyPropertyChanged for properties, so you won't be getting any change notifications. You might want to learn more about the MVVM pattern.

UserControl DataContext Binding

I have three projects in my solution:
My main WPF Application which contains a MainWindow + MainViewModel
UserControl Library with a UserControl (ConfigEditorView)
UIProcess class with the ViewModel for the UserControl (ConfigEditorViewModel)
In my MainWindow I want to use the UserControl with the ViewModel of UIProcess.
First I set the UserControl in my MainWindow:
<TabItem Header="Editor">
<Grid>
<cel:ConfigEditorView DataContext="{Binding ConfEditModel, NotifyOnSourceUpdated=True, NotifyOnTargetUpdated=True, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</TabItem>
I don't know which of these properties I need here, so I put all together but it still doesn't work.
Then I've set this in my MainViewModel:
public ConfigEditorViewModel ConfEditModel { get; set; }
With simple method that is bound to a Button:
private void doSomething()
{
ConfEditModel = new ConfigEditorViewModel("Hello World");
}
My ConfigEditorViewModel looks basically like this:
public class ConfigEditorViewModel : ViewModelBase
{
private string _Description;
public string Description
{
get
{
return _Description;
}
set
{
_Description = value;
base.RaisePropertyChanged();
}
}
public ConfigEditorViewModel(string t)
{
Description = t;
}
}
The description is bound to a TextBox in my UserControl.
<TextBox Grid.Row="1" Grid.Column="1" Margin="0,0,0,10" Text="{Binding Description}"/>
When I start the application and click the Button the TextBox should contain "Hello World" but it's empty.
What I've done wrong?
i gave you a general answer:
within a "real(a usercontrol you wanna use with different viewmodels with different property names)" usercontrol you bind just to your own DependencyProperties and you do that with ElementName or RelativeSource binding and you should never set the DataContext within a UserControl.
<UserControl x:Name="myRealUC" x:class="MyUserControl">
<TextBox Text="{Binding ElementName=myRealUC, Path=MyOwnDPIDeclaredInMyUc, Path=TwoWay}"/>
<UserControl>
if you do that you can easily use this Usercontrol in any view like:
<myControls:MyUserControl MyOwnDPIDeclaredInMyUc="{Binding MyPropertyInMyViewmodel}"/>
and for completeness: the Dependency Property
public readonly static DependencyProperty MyOwnDPIDeclaredInMyUcProperty = DependencyProperty.Register(
"MyOwnDPIDeclaredInMyUc", typeof(string), typeof(MyUserControl), new PropertyMetadata(""));
public bool MyOwnDPIDeclaredInMyUc
{
get { return (string)GetValue(MyOwnDPIDeclaredInMyUcProperty); }
set { SetValue(MyOwnDPIDeclaredInMyUcProperty, value); }
}
Your view models (and, optionally, models) need to implement INotifyPropertyChanged.
Binding's aren't magic. There is no inbuilt mechanism that allows for code to be notified when a plain old property's value changes. You'd have to poll it in order to check to see if a change happened, which would be very bad, performance-wise.
So bindings will look at the objects they are bound against and see if they implement INotifyPropertyChanged and, if so, will subscribe to the PropertyChanged event. That way, when you change a property and fire the event, the binding is notified and updates the UI.
Be warned, you must implement the interface and use it correctly. This example says it's for 2010, but it works fine.

Bound TextBox does not update

I have a ComboBox bound to an ObservableCollection of objects (with several properties). The Combo Box accurately displays the desired property of all objects and I can select any item from the Combo as expected.
<ComboBox Height="23" Name="comboBox1" Width="120" Margin="5" ItemsSource="{Binding Issues}" DisplayMemberPath="Issue" SelectedValuePath="Issue" SelectedValue="{Binding Path=Issues}" IsEditable="False" SelectionChanged="comboBox1_SelectionChanged" LostFocus="comboBox1_LostFocus" KeyUp="comboBox1_KeyUp" Loaded="comboBox1_Loaded" DropDownClosed="comboBox1_DropDownClosed" IsSynchronizedWithCurrentItem="True" />
I have a series of text boxes which are supposed to display other properties of the selected object. This works fine too.
<TextBox Height="23" Name="textBox5" Width="59" IsReadOnly="True" Text="{Binding Issues/LastSale, StringFormat={}{0:N4}}" />
<TextBox Height="23" Name="textBox9" Width="90" IsReadOnly="True" Text="{Binding Path=Issues/LastUpdate, Converter={StaticResource TimeConverter}}" />
BUT... The properties of ObservableCollection are updated in the Code-Behind on a regular basis and I make a change to the OC by either adding or removing a dummy object in it every time the properties are updated. (I found this simpler than other solutions).
BUT...the data in the TextBoxes DO NOT change! :-( If I select a different object from the ComboBox I get updated info, but it does not change when the OC is changed.
The OC is composed of a bunch of these Objects:
public class IssuesItems
{
public String Issue { get; set; }
public Double LastSale { get; set; }
public DateTime LastUpdate { get; set; }
...
}
The OC is defined as:
public ObservableCollection<IssuesItems> Issues { get; set; }
and instantiated:
this.Issues = new ObservableCollection<IssuesItems>();
What am I doing wrong? Everything I read says that when the LastSale and LastUpdate properties are changed in the OC (and I do something to force an update of the OC) the data in the text boxes ought to change.
ObservableCollection implements INotifyCollectionChanged which allows GUI to refresh when any item is added or deleted from collection (you need not to worry about doing it manually).
But like i mentioned this is restricted to only addition/deletion of items from collection but if you want GUI to refresh when any underlying property gets changed, your underlying source class must implement INotifyPropertyChanged to give notification to GUI that property has changed so refresh yourself.
IssuesItems should implement INPC interface in your case.
Refer to this - How to implement INotifyPropertyChanged on class.
public class IssuesItems : INotifyPropertyChanged
{
private string issue;
public string Issue
{
get { return issue; }
set
{
if(issue != value)
{
issue= value;
// Call OnPropertyChanged whenever the property is updated
OnPropertyChanged("Issue");
}
}
}
// Declare the event
public event PropertyChangedEventHandler PropertyChanged;
// Create the OnPropertyChanged method to raise the event
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
Implement other properties just like Issue as mentioned above.

Categories