Observable FromEventPattern INotifyCollectionChanged Bug - wpf

I have been trying to figure out how to do this. When I have a custom control that I am trying to watch collection changes in the controls ItemsSource. I want to use Reactive Extensions so I can easily dispose of the event watching and avoid the complex weak event pattern. When my ItemsSource is set to a ListCollectionView (From CollectionViewSource in xaml) or a ReadOnlyObservableCollection the FromEventPattern throws an error:
"Could not find event 'CollectionChanged' on object of type 'System.Windows.Data.ListCollectionView'."
These classes hide the INotifyCollectionChanged by explicitly implementing INotifyCollectionChanged.
Here is my Code. Is this a bug in Reactive Extensions or am I doing it wrong?
IDisposable WatchCollection(INotifyCollectionChanged inc)
{
return Observable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(inc, "CollectionChanged").Subscribe(ep =>
{
ItemsChanged();
Refresh();
});
}

I think the problem is here.
The correct implementation should be:
private static IObservable<EventPattern<TSender, TEventArgs>> FromEventPattern_<TSender, TEventArgs>(object target, string eventName, IScheduler scheduler)
{
return FromEventPattern_<TSender, TEventArgs, EventPattern<TSender, TEventArgs>>(typeof(TSender), target, eventName, (sender, args) => new EventPattern<TSender, TEventArgs>(sender, args), scheduler);
}
Filed issue 1239.
In the meantime, you can change it to:
return Observable
.FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
e => inc.CollectionChanged += e,
e => inc.CollectionChanged -= e)
.Subscribe(ep =>
{
ItemsChanged();
Refresh();
});

Related

BindableCollection changes when other ObservableCollection changes

Is there a way to update an ObservableCollection with the items which are added/deleted in other ObservableCollection?
How can I update my ViewModel's BindableCollection when items are added, removed in FullyObservableCollection?
It is important to note I am trying to use MVVM pattern with Caliburn.Micro.
VieModel
private BindableCollection<Employees> _employees = new BindableCollection(OracleConnector.GetEmployeeRepositorys());
public BindableCollection<Employees> Employees
{
get
{
return _employees;
}
set
{
OracleConnector.List.CollectionChanged += (sender, args) =>
{
_employees = OracleConnector.List;
};
NotifyOfPropertyChange(() => Employees);
}
}
OracleConnector
public class OracleConnector
{
public static FullyObservableCollection<Employees> List = new FullyObservableCollection<Employees>();
public static FullyObservableCollection<Employees> GetEmployeeRepositorys()
{
using (IDbConnection cnn = GetDBConnection("localhost", 1521, "ORCL", "hr", "hr"))
{
var dyParam = new OracleDynamicParameters();
try
{
var output = cnn.Query<Employees>(OracleDynamicParameters.sqlSelect, param: dyParam).AsList();
foreach (Employees employees in output)
{
List.Add(employees);
}
}
catch (OracleException ex)
{
MessageBox.Show("Connection to database is not available.\n" + ex.Message, "Database not available", MessageBoxButton.OK, MessageBoxImage.Error);
}
return List;
}
}
}
I am able to detect if changes are made in the FullyObservableCollection but I don't know how to pass them to the ViewModel.
Use the IEventAggregator in the OracleConnector class when you add a new employee. Publish a EmployeeAddedMessage which contains the new employee. Make sure you publish on the correct thread too. You are likely to need to use PublishOnUiThread method. The ShellViewModel is then able to implement the IHandle<EmployeeAddedMessage> as a method probably called Handle(EmployeeAddedMessage msg). Inside the Handle method you can then add the Employee to the appropriate Employee collection.
You may need to add the OracleConnector to your application bootstrapper as well as the EventAggregator class that Caliburn Micro provide. Your ShellViewModel will also need to call the Subscribe(this) method on the event aggregator. Both the OracleConnector and ShellViewModel need to be using the same instance of the event notifier. So make sure you register the event aggregator as a singleton.
See here for more details about using the event notification. Also, my application here uses event notification for application events.

Reactive UI with Datagrids

Currently my RadDatagrid1 has a Cell_click action for the RadDatagrid1, and when a ClientName is selected, that client info is projected in DataGrid2.
code within Mouse Double Click:
private void Cell_click(object sender, GridViewSelectedCellsChangedEventArgs e)
{
Var Details = (from info in contacts
where info.ClientName = sender.CurrentCell.ToString()
select new {info.ClientName, info.ClientAddress, Info.ClientNumber});
DataGrid2.ItemsSource = Details.ToList();
}
This is currently what i have but, it should be a reactive UI.
An example of reactitve UI i was told to look at was this in the GridViewModel:
this.WhenAny(x => x.Forename, x => x.Surname, x => x.City, (p1, p2, p3) => Unit.Default).Subscribe(x => Filter());
But that doesn't quite make sense to me. If I could get guidance and tips how to convert this to reactive UI please.
I am a newbie to Reactive UI and my experience so far has been through trial and error, due to the lack of documentation. So my method below might not be correct.
Make sure you have a ViewModel backing your WPF control (see this page)
Your ViewModel should look something like:
public class ViewModel : ReactiveObject {
// ClientInfo type is the type of object you want to bind to the DataGrid
private ClientInfo clientInfo;
public ClientInfo ClientInfo {
set { this.RaiseAndSetIfChanged(ref clientInfo, value); }
get { return clientInfo; }
}
// Move contacts IEnumerable/IQueryable to your ViewModel
private IQueryable<ClientInfo> contacts;
public LoadInfo(string clientName) {
ClientInfo = (from info in contacts
where info.ClientName = clientName
select new {info.ClientName, info.ClientAddress, Info.ClientNumber})
}
}
Make sure your View (the control class) implements IViewFor<T> where T is the type of your View Model. Bind views according to the documentation here.
Do something like this for your View:
// Implement the methods on that interface, which I will not include below
public partial class View : IViewFor<ViewModel> {
private ICommand loadClientInfo;
public View() { // constructor
InitializeComponent(); // Don't forget this
// Binds the data in your ViewModel with the ItemSource of your DataGrid
this.OneWayBind(ViewModel, vm => vm.ClientInfo, x => x.DataGrid2.ItemSource);
}
private void Cell_click(object sender, GridViewSelectedCellsChangedEventArgs e)
{
ViewModel.LoadInfo(sender.CurrentCell.ToString());
}
}

How to send notification about the change of composite collection

I've got a composite collection. After modifications on its items from code behind I want the View to get updated. But I don't know how to notify the view. I tried the INotifyCollectionChanged, but it didn't work for me.
protected ObservableCollection<ScriptParameterComboItem> cItems
public virtual CompositeCollection CItems
{
get
{
return new CompositeCollection {new CollectionContainer {Collection = cItems}};
}
}
public void ConvertValue(params object[] parameters)
{
string newAverageOption = DisplayValueConverter.Convert(1, parameters);
var enumItem = cItems[1];
enumItem.Value = newAverageOption;
RaiseCollectionChanged("CItems");
}
protected void RaiseCollectionChanged(string property)
{
if(CollectionChanged != null)
CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add));
}
Your ScriptParameterComboItem class must implement INotifyPropertyChanged. So when changing it's properties, listeners will be notified. Using ObservableCollection helps listeners to be notified when something is added to the collection or removed from. Not changing the actual data within every single item.

Async code in .NET 4.0

I have the following code running in a WPF app:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
object obj = new object();
Collection.Add(obj);
Collection.CollectionChanged += new NotifyCollectionChangedEventHandler(delegate(object sender2, NotifyCollectionChangedEventArgs e2)
{
if (Collection.Count == 0)
App.Current.MainWindow.Close();
});
Task.Factory.StartNew(() =>
{
//Do long running process
Collection.Remove(obj); //this errors out
});
}
private ObservableCollection<object> Collection = new ObservableCollection<object>();
}
I get the error System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it.
I was under the impression that Task.Factory.StartNew queued up an async task, so the thread should be the same, no?
Task.Factory.StartNew executes your action in the default TaskScheduler, so it will run in the ThreadPool.
ObservableCollection is not thread-safe. It means that your CollectionChanged handler, which performs operations on UI controls ( App.Current.MainWindow.Close() ) is not going to be executed in the UI thread because the collection modification is being done in your Task's action, causing the error you are seeing.
If you only need to interact with the UI in your handler, you can use the dispatcher:
Collection.CollectionChanged += new NotifyCollectionChangedEventHandler(delegate(object sender2, NotifyCollectionChangedEventArgs e2)
{
if (Collection.Count == 0)
this.Dispatcher.BeginInvoke((Action)(()=> App.Current.MainWindow.Close()));
});
If you need to bind to it, consider using a thread-safe implementation. See this.
Just to add to Arthur's answer, in my real application (not the sample code above) I needed to do this from an MvvmLight view model. To access the dispatcher from a ViewModel:
Inside App, add the following:
static App()
{
DispatcherHelper.Initialize();
}
And then instead of calling this.Dispatcher, because a ViewModel has no reference to the Dispatcher, the following will work:
DispatcherHelper.UIDispatcher.BeginInvoke((Action)(() => App.Current.MainWindow.Close()));

CollectionViewSource Filter not refreshed when Source is changed

I have a WPF ListView bound to a CollectionViewSource. The source of that is bound to a property, which can change if the user selects an option.
When the list view source is updated due to a property changed event, everything updates correctly, but the view is not refreshed to take into account any changes in the CollectionViewSource filter.
If I attach a handler to the Changed event that the Source property is bound to I can refresh the view, but this is still the old view, as the binding has not updated the list yet.
Is there a decent way to make the view refresh and re-evaluate the filters when the source changes?
Cheers
Updating the CollectionView.Filter based on a PropertyChanged event is not supported by the framework.
There are a number of solutions around this.
1) Implementing the IEditableObject interface on the objects inside your collection, and calling BeginEdit and EndEdit when changing the property on which the filter is based.
You can read more about this on the Dr.WPF's excellent blog here : Editable Collections by Dr.WPF
2) Creating the following class and using the RefreshFilter function on the changed object.
public class FilteredObservableCollection<T> : ObservableCollection<T>
{
public void RefreshFilter(T changedobject)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, changedobject, changedobject));
}
}
Example:
public class TestClass : INotifyPropertyChanged
{
private string _TestProp;
public string TestProp
{
get{ return _TestProp; }
set
{
_TestProp = value;
RaisePropertyChanged("TestProp");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
FilteredObservableCollection<TestClass> TestCollection = new FilteredObservableCollection<TestClass>();
void TestClass_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "TestProp":
TestCollection.RefreshFilter(sender as TestClass);
break;
}
}
Subscribe to the PropertyChanged event of the TestClass object when you create it, but don't forget to unhook the eventhandler when the object gets removed, otherwise this may lead to memory leaks
OR
Inject the TestCollection into the TestClass and use the RefreshFilter function inside the TestProp setter.
Anyhow, the magic here is worked by the NotifyCollectionChangedAction.Replace which updates the item entirely.
Are you changing the actual collection instance assigned to the CollectionViewSource.Source, or are you just firing PropertyChanged on the property that it's bound to?
If the Source property is set, the filter should be recalled for every item in the new source collection, so I'm thinking something else is happening. Have you tried setting Source manually instead of using a binding and seeing if you still get your behavior?
Edit:
Are you using CollectionViewSource.View.Filter property, or the CollectionViewSource.Filter event? The CollectionView will get blown away when you set a new Source, so if you had a Filter set on the CollectionView it won't be there anymore.
I found a specific solution for extending the ObservableCollection class to one that monitors changes in the properties of the objects it contains here.
Here's that code with a few modifications by me:
namespace Solution
{
public class ObservableCollectionEx<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e != null) // There's been an addition or removal of items from the Collection
{
Unsubscribe(e.OldItems);
Subscribe(e.NewItems);
base.OnCollectionChanged(e);
}
else
{
// Just a property has changed, so reset the Collection.
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
protected override void ClearItems()
{
foreach (T element in this)
element.PropertyChanged -= ContainedElementChanged;
base.ClearItems();
}
private void Subscribe(IList iList)
{
if (iList != null)
{
foreach (T element in iList)
element.PropertyChanged += ContainedElementChanged;
}
}
private void Unsubscribe(IList iList)
{
if (iList != null)
{
foreach (T element in iList)
element.PropertyChanged -= ContainedElementChanged;
}
}
private void ContainedElementChanged(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(e);
// Tell the Collection that the property has changed
this.OnCollectionChanged(null);
}
}
}
Maybe a bit late to the party but just in case
You can also use CollectionViewSource.LiveSortingProperties
I found it through this blog post.
public class Message : INotifyPropertyChanged
{
public string Text { get; set; }
public bool Read { get; set; }
/* for simplicity left out implementation of INotifyPropertyChanged */
}
public ObservableCollection<Message> Messages {get; set}
ListCollectionView listColectionView = (ListCollectionView)CollectionViewSource.GetDefaultView(Messages);
listColectionView.IsLiveSorting = true;
listColectionView.LiveSortingProperties.Add(nameof(Message.Read));
listColectionView.SortDescriptions.Add(new SortDescription(nameof(Message.Read), ListSortDirection.Ascending));
I found a relatively simple method to do this.
I changed the readonly ICollectionView property to get/set and added the raised property event:
Property TypeFilteredCollection As ICollectionView
Get
Dim returnVal As ICollectionView = Me.TypeCollection.View
returnVal.SortDescriptions.Add(New SortDescription("KeyName", ListSortDirection.Ascending))
Return returnVal
End Get
Set(value As ICollectionView)
RaisePropertyChanged(NameOf(TypeFilteredCollection))
End Set
End Property
Then to update, i just used:
Me.TypeFilteredCollection = Me.TypeFilteredCollection
This clearly won't work if you don't have somewhere to trigger that update though.

Resources