Why wont UI update on RaisePropertyChanged? - c#

I have a static IList which acts as a repository in a static class:
//static class Settings
public static IList RecentSearchedRepo = new ObservableCollection<object>();
and an IList located in another class which I bind my UI grid to :
//component class
private IList recentsearch = new ObservableCollection<object>();
public IList RecentSearch
{
get
{
return recentsearch;
}
set
{
recentsearch = value;
RaisePropertyChanged("RecentSearch");
}
}
I add objects to RecentSearchedRepo :
RecentSearchedRepo.add(searchitem)
then set RecentSearch to the static list
RecentSearch = Settings.RecentSearchedRepo;
XAML snippet:
<GridLayout:RecentSearchGrid x:Name="recentSearchGrid" ItemsSource="{Binding RecentSearch}" />
snippet from RecentSearchGrid class which extends UserControl:
public IList ItemsSource
{
get
{
return GetValue(ItemsSourceProperty) as IList;
}
set
{
SetValue(ItemsSourceProperty, value);
}
}
private static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IList), typeof(RecentSearchGrid), new PropertyMetadata(null, OnItemsSourcePropertyChanged));
private static void OnItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RecentSearchGrid source = d as RecentSearchGrid;
if (source != null)
{
source.setListforgrid(source.ItemsSource);
}
}
The problem is when I add the first item to RecentSearchedRepo the UI is updated , but on every subsequent add the UI does not update.

Instead of:
RecentSearch = Settings.RecentSearchedRepo;
Try doing:
RecentSearch.Clear();
var freshData = Settings.RecentSearchedRepo;
if (freshData != null)
foreach (var item in freshData)
RecentSearch.Add(item);
You were killing the binding by reassigning the reference.
EDIT: After yours
You're doing it backwards: that OnItemsSourcePropertyChanged shouldn't be setting the source, it shouldn't be there at all actually.
You must bind, in RecentSearchGrid.xaml, to the ItemsSource dependency property declared in RecentSearchGrid.xaml.cs

I don't think that there's enough information here to answer your question. The following simple application mirrors the scenario that I see described in the question and it works as expected:
// MySettings.cs
public static class MySettings
{
public static IList RecentSearchedRepo = new ObservableCollection<object>();
}
// MyVm.cs
public class MyVm : INotifyPropertyChanged
{
private IList recentSearch = new ObservableCollection<object>();
public event PropertyChangedEventHandler PropertyChanged;
public MyVm()
{
this.RecentSearch = MySettings.RecentSearchedRepo;
}
public IList RecentSearch
{
get { return recentSearch; }
set
{
recentSearch = value;
this.RaisePropertyChanged("RecentSearch");
}
}
private void RaisePropertyChanged(string p)
{
if (this.PropertyChanged != null) this.PropertyChanged(this, new PropertyChangedEventArgs(p));
}
}
// MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
// Initialization as described in the question
MySettings.RecentSearchedRepo.Add("SearchItem1");
MySettings.RecentSearchedRepo.Add("SearchItem2");
this.DataContext = new MyVm();
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
// Add a new item later
MySettings.RecentSearchedRepo.Add("NewlyAddedSearchItem");
}
}
// MainWindow.xaml
<Window x:Class="ScratchWpf.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">
<DockPanel>
<Button DockPanel.Dock="Bottom" Content="Add new Search Item" Click="Button_Click_1" />
<ListBox ItemsSource="{Binding RecentSearch}" />
</DockPanel>
</Window>
I'm going to try putting on my psychic hat and ask if, perhaps, you are adding additional items to the wrong collection. Does the collection get recreated and placed at the binding after a single item is added, but later items are added to the original collection instead of the new one?
Given that you stated RecentSearchGrid is a UserControl, we can also infer that the implementation of ItemsSource may be a custom one rather than the standard one that would be inherited from an ItemsControl. It's possible that the RecentSearchGrid is breaking the binding incorrectly in there somehow.
I agree with Baboon. What is the purpose of OnItemsSourcePropertyChanged? In a typical implementation, I wouldn't expect that to be there.

The problem may be as follow:
OnItemsSourcePropertyChanged will not get called, if the instance does not change.
From WPF point of view, when you RaisePropertyChangeEvent, but the instance of the bound collection does not change, PropertyChange handler will not be called at all.
Is Settings.RecentSearchedRepo the same instance through the lifetime of the app?

Related

WPF - DataGrid doesn't show list

My goal is to output a list in a datagrid, but this doesn't work and the datagrid is empty.
I tried to display the list in an other way and it did (but I can't remember what it was) and it worked, except for it not being in a datagrid but just data. I have changed up some things, but back then it reached the end and got displayed.
ViewModel in Mainwindow:
public class ViewModel
{
public List<ssearch> Items { get; set; }
private static ViewModel _instance = new ViewModel();
public static ViewModel Instance { get { return _instance; } }
}
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
//For simplicity, let's say this window opens right away
var Mdata = new MDataWindow { DataContext = DataContext };
Mdata.Show();
}
Other Window for data display:
string searchParam = "status = 1";
public MDataWindow()
{
InitializeComponent();
}
private void AButton_Click(object sender, RoutedEventArgs e)
{
MainWindow.ViewModel.Instance.Items = Search(searchParam);
}
public List<ssearch> Search(string where)
{
{
//Lots of stuff going on here
}
return returnList;
}
And in WPF:
<Window x:Class="WPFClient.MDataWindow"
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:WPFClient"
mc:Ignorable="d"
Title="MDataWindow" Height="Auto" Width="Auto">
<StackPanel>
<Button x:Name="AButton" Click="AButton_Click" Content="Load" />
<DataGrid ItemsSource="{Binding Items}" />
</StackPanel>
</Window>
I have no clue where the error is and tried to strip the code down as much as possible without killing error sources. The Datagrid just stays empty when I press the "Load" button.
EDIT:
I tried to convert the list into an observableColletion before passing it to the ViewModel, but this didn't work. I am working with a library, which I am not sure how to use observableCollection with, so I converted it instead of using it right away:
VM:
public ObservableCollection<Product> Items { get; set; }
Data Window:
List<Product> pp = Search_Products(searchParam);
var oc = new ObservableCollection<Product>(pp);
MainWindow.ViewModel.Instance.Items = oc;
First, change your List<Product> to an ObservableCollection<Product> as this will help to display the Items of the list on Add/Remove immediately.
This is because ObservableCollection implements the INotifyCollectionChanged interface to notify your target(DataGrid) to which it is bound, to update its UI.
Second, your binding can never work as expected due to changed reference of your collection.
private void AButton_Click(object sender, RoutedEventArgs e)
{
// You are changing your Items' reference completely here, the XAML binding
// in your View is still bound to the old reference, that is why you're seeing nothing.
//MainWindow.ViewModel.Instance.Items = Search(searchParam);
var searchResults = Search(searchParam);
foreach(var searchResult in searchResults)
{
MainWindow.ViewModel.Instance.Items.Add(searchResult);
}
}
Make sure you have changed the List to ObservableCollection upon running the Add loop, else you will get an exception saying the item collection state is inconsistent.
The ViewModel class should implement the INotifyPropertyChanged interface and raise its PropertyChanged event whenever Items is set to a new collection:
public class ViewModel : INotifyPropertyChanged
{
private List<ssearch> _items;
public List<ssearch> Items
{
get { return _items; }
set { _items = value; OnPropertyChanged(); }
}
private static ViewModel _instance = new ViewModel();
public static ViewModel Instance { get { return _instance; } }
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
This is required to notify the view regardless of the type of Items.
If you change the type of Items to ObservableCollection<T>, you should initialize the collection in the view model once:
public class ViewModel
{
public ObservableCollection<ssearch> Items { get; } = new ObservableCollection<ssearch>();
private static ViewModel _instance = new ViewModel();
public static ViewModel Instance { get { return _instance; } }
}
...and then add items to this collection instead of setting the property to a new one:
private void AButton_Click(object sender, RoutedEventArgs e)
{
MainWindow.ViewModel.Instance.Items.Clear();
var search = Search(searchParam);
if (search != null)
foreach (var x in search)
MainWindow.ViewModel.Instance.Items.Add(x);
}

WPF OneWay binding works only once

I have a View that have two comboboxes. One is where user selects routing pipe type name, and the other where there should be a list of available diameters for the chosen pipe type.
Whenever user selects the pipe type, the other combobox should update the list of available diameters.
AvailableDiameters and RoutingPipeTypeName properties are static in Context class, that implements INotifyPropertyChanged interface. In xaml I've set the bindings to these properties, in code behind also the DataContext.
The problem is that the list of diameters get's updated only once, when the view is initialized.
When debugging I can see that the properties backing field's values are updated properly when selection on pipe type name is changed, only in the UI the available diameters list is not updated...
Context class:
public class Context : INotifyPropertyChanged
{
public static Context This { get; set; } = new Context();
public static string RoutingPipeTypeName
{
get => _routingPipeTypeName;
set
{
if (_routingPipeTypeName != value)
{
_routingPipeTypeName = value;
This.OnPropertyChanged(nameof(RoutingPipeTypeName));
}
}
}
public static List<double> AvailableDiameters
{
get => _availableDiameters;
set
{
//check if new list's elements are not equal
if (!value.All(_availableDiameters.Contains))
{
_availableDiameters = value;
This.OnPropertyChanged(nameof(AvailableDiameters));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
xaml:
<ComboBox Width="80" SelectedValue="{Binding Path=RoutingPipeTypeName, Mode=OneWayToSource}">
<ComboBoxItem Content="Example pipe type 1"></ComboBoxItem>
<ComboBoxItem Content="Example pipe type 2"></ComboBoxItem>
</ComboBox>
<ComboBox Width="80" SelectedValue="{Binding Path=RoutingDiameter, Mode=OneWayToSource}" ItemsSource="{Binding Path=AvailableDiameters, Mode=OneWay}">
</ComboBox>
code behind:
public Context AppContext => Context.This;
public MyView()
{
InitializeComponent();
Instance = this;
DataContext = AppContext;
}
And the client class that is responsible for updating the list of diameters:
public void InitializeUIContext()
{
Context.This.PropertyChanged += UIContextChanged;
if (Cache.CachedPipeTypes.Count > 0)
Context.RoutingPipeTypeName = Cache.CachedPipeTypes.First().Key;
}
private void UIContextChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Context.RoutingPipeTypeName))
{
Context.AvailableDiameters = Cache.CachedPipeTypes.First().Value.GetAvailableDiameters();
}
}
I expected such set-up would update the diameters combobox each time the selection is changed on the pipe types property.
Instead it updates it only once, when the view is initialized... Why?
Do not use static properties for binding to an object (which you have correctly passed to the DataContext of your view).
Declare the properties without the static modifier and replace This.OnPropertyChanged by OnPropertyChanged:
public string RoutingPipeTypeName
{
get => _routingPipeTypeName;
set
{
if (_routingPipeTypeName != value)
{
_routingPipeTypeName = value;
OnPropertyChanged(nameof(RoutingPipeTypeName));
}
}
}
You should also remove the static This from your Context class and simply write
public Context AppContext { get; } = new Context();

How to bind the same dynamic context menu to a dynamic number of user controls?

Imagine this:
You have a number of controls which you want to have a context menu, the same menu. That context menu will be dynamic and can change at any time. So you create a static class like this:
public static class EmployeeContextMenu
{
private static ContextMenu EmployeeMenu { get; set; }
static EmployeeContextMenu()
{
EmployeeMenu = new ContextMenu();
}
public static ContextMenu Get()
{
return EmployeeMenu;
}
public static void Set(List<String> employees)
{
EmployeeMenu = new ContextMenu();
MenuItem mi;
foreach (var item in employees)
{
mi = new MenuItem();
mi.Header = item;
EmployeeMenu.Items.Add(mi);
}
}
}
And at some point you need to change the menu items and use the Set method above, works perfectly fine.
Somehow you want to bind your controls context menu to the Get method.
Might be achieved like:
<Window.Resources>
<ObjectDataProvider ObjectType="{x:Type local:EmployeeContextMenu}" MethodName="Get" x:Key="myCM" />
</Window.Resources>
<Grid>
<TextBlock Name="tb" ContextMenu="{Binding Source={StaticResource myCM}}" />
</Grid>
And of course you want the controls context menu to be updated as soon as the static Set method is called.
But above, something is basically wrong with the binding...no context menu shows up at all.
So how would you set the binding? Datacontext, datasource? Where and how?
It would of course work if you implement
tb.ContextMenu = EmployeeContextMenu.Get();
But that would be a bad solution if you have an unknown number of dynamically created user controls which you badly want to have the mentioned context menu which will be updated on the fly.
what you can do is to restructure it a bit as you need change notification for the EmployeeMenu property in order to update after Set() method
so start by the class itself
namespace CSharpWPF
{
public sealed class EmployeeContextMenu : INotifyPropertyChanged
{
private EmployeeContextMenu()
{
//prevent init
}
static EmployeeContextMenu()
{
Instance = new EmployeeContextMenu();
Instance.EmployeeMenu = new ContextMenu();
}
public static EmployeeContextMenu Instance { get; private set; }
private ContextMenu _menu;
public ContextMenu EmployeeMenu
{
get
{
return _menu;
}
set
{
_menu = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("EmployeeMenu"));
}
}
public void Set(List<String> employees)
{
EmployeeMenu = new ContextMenu();
MenuItem mi;
foreach (var item in employees)
{
mi = new MenuItem();
mi.Header = item;
EmployeeMenu.Items.Add(mi);
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
I have implemented INotifyPropertyChanged
made it non static with private constructor to avoid init
sealed the class to prevent deriving further
added static Instance property to look from xaml
rest is quite basic
xaml
<Grid xmlns:l="clr-namespace:CSharpWPF">
<TextBlock Name="tb" ContextMenu="{Binding EmployeeMenu,Source={x:Static l:EmployeeContextMenu.Instance}}" />
</Grid>
to call Set() method
EmployeeContextMenu.Instance.Set(list);
so the whole idea it to enable the property changed notification while keeping as static reference

Alter hosted Winform control from ViewModel

Sorry to be cliche... but I'm pretty new to WPF and MVVM so I'm not sure how to handle this properly. I have a WinForms control within one of my views that I need to modify in it's code behind when an event is raised in the ViewModel. My view's datacontext is inherited so the viewmodel is not defined in the views constructor. How would I go about properly handling this? I am not using any frameworks with built in messengers or aggregators. My relevant code is below. I need to fire the ChangeUrl method from my ViewModel.
EDIT: Based on the suggestion from HighCore, I have updated my code. I am still not able to execute the ChangeUrl method however, the event is being raised in my ViewModel. What modifications need to be made??
UserControl.xaml
<UserControl ...>
<WindowsFormsHost>
<vlc:AxVLCPlugin2 x:Name="VlcPlayerObject" />
</WindowsFormsHost>
</UserControl>
UserControl.cs
public partial class VlcPlayer : UserControl
{
public VlcPlayer()
{
InitializeComponent();
}
public string VlcUrl
{
get { return (string)GetValue(VlcUrlProperty); }
set
{
ChangeVlcUrl(value);
SetValue(VlcUrlProperty, value);
}
}
public static readonly DependencyProperty VlcUrlProperty =
DependencyProperty.Register("VlcUrl", typeof(string), typeof(VlcPlayer), new PropertyMetadata(null));
private void ChangeVlcUrl(string newUrl)
{
//do stuff here
}
}
view.xaml
<wuc:VlcPlayer VlcUrl="{Binding Path=ScreenVlcUrl}" />
ViewModel
private string screenVlcUrl;
public string ScreenVlcUrl
{
get { return screenVlcUrl; }
set
{
screenVlcUrl = value;
RaisePropertyChangedEvent("ScreenVlcUrl");
}
}
WPF does not execute your property setter when you Bind the property, instead you must define a Callback method in the DependencyProperty declaration:
public string VlcUrl
{
get { return (string)GetValue(VlcUrlProperty); }
set { SetValue(VlcUrlProperty, value); }
}
public static readonly DependencyProperty VlcUrlProperty =
DependencyProperty.Register("VlcUrl", typeof(string), typeof(VlcPlayer), new PropertyMetadata(null, OnVlcUrlChanged));
private static void OnVlcUrlChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var player = obj as VlcPlayer;
if (obj == null)
return;
obj.ChangeVlcUrl(e.NewValue);
}
private void ChangeVlcUrl(string newUrl)
{
//do stuff here
}

Add an item to ListBox and get the ListBoxItem

UPDATE:
I uploaded a demo trying to explain my problem clear. Download it here.
I'm developing an manager class that deals with the selection of ListBox. (The default selection function provided by ListBox cannot satisfy me requirements)
So, when an item is added to the ListBox, my manager class should get the corresponding ListBoxItem and make it selected or unselected.
Although I think ItemContainerGenerator.ItemsChanged should tell some information of the newly added item, it provides same event arg when ListBox.Items.Add is called multiple times(with different parameter), which confuses me a lot. Can anyone tell me how to get the newly generated ListBoxItem for the newly added item.
Code to demonstrate the problem:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:System="clr-namespace:System;assembly=mscorlib"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Button Content="Add two items" Click="Button_Click_1"/>
<ListBox Name="listBox">
<System:Int32>1</System:Int32>
<System:Int32>2</System:Int32>
<System:Int32>3</System:Int32>
</ListBox>
</StackPanel>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SelectionManager selectionManager = new SelectionManager();
selectionManager.Join(listBox);
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
listBox.Items.Add(4);
listBox.Items.Add(5);
}
}
Here, in Button_Click, two items are added to the listBox, and selectionManager should get the ListBoxItem at same time.
class SelectionManager
{
public void Join(ListBox element)
{
element.ItemContainerGenerator.ItemsChanged += ItemContainerGenerator_ItemsChanged;
}
private List<int> listBoxItemPendingJoinIndexes = new List<int>();
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
Contract.Requires(listBoxItemPendingJoinIndexes.Count > 0);
ItemContainerGenerator generator = (ItemContainerGenerator)sender;
if (generator.Status != GeneratorStatus.ContainersGenerated)
return;
generator.StatusChanged -= ItemContainerGenerator_StatusChanged;
foreach (var index in listBoxItemPendingJoinIndexes)
{
ListBoxItem listBoxItem = (ListBoxItem)generator.ContainerFromIndex(index);
Join(listBoxItem);
}
listBoxItemPendingJoinIndexes.Clear();
}
void ItemContainerGenerator_ItemsChanged(object sender, ItemsChangedEventArgs e)
{
ItemContainerGenerator generator = (ItemContainerGenerator)sender;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
listBoxItemPendingJoinIndexes.Add(e.Position.Index
+ e.Position.Offset);//same e.Position
generator.StatusChanged += ItemContainerGenerator_StatusChanged;
break;
}
}
}
If your items source is an ObservableCollection, you can use OnCollectionChanged event, which has a NotifyCollectionChangedEventArgs Check NewItems for the list of new items involved in the change.
Maybe create a cutsom listbox class and override the methods?
public class SafeListBox : ListBox
{
delegate void insertDelegate(int i, object o);
public SafeListBox()
{
this.Items = new CustomObjectCollection(this);
}
public new CustomObjectCollection Items
{
get;
set;
}
public class CustomObjectCollection : ListBox.ObjectCollection
{
private ListBox listBox = null;
public CustomObjectCollection(ListBox listBox) : base(listBox)
{
this.listBox = listBox;
}
public new void Insert(int index, object item)
{
if (listBox.InvokeRequired)
{
insertDelegate setTextDel = delegate(int i, object o)
{
base.Insert(i, o);
};
try
{
listBox.Invoke(setTextDel, new object[] { index, item });
}
catch
{
}
}
else
{
base.Insert(index,item);
}
}
}
}
I'm not sure if i fully understand the question. But if it is only about how to manipulate a newly created ListBoxItem once before it is displayed, you could create a derived ListBox and just override the PrepareContainerForItemOverride method.
public class MyListBox : ListBox
{
protected override void PrepareContainerForItemOverride(
DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
var listBoxItem = element as ListBoxItem;
...
}
}

Categories