I have been looking into using Rx in an MVVM framework. The idea is to use 'live' LINQ queries over in-memory datasets to project data into View Models to bind with.
Previously this has been possible with the use of INotifyPropertyChanged/INotifyCollectionChanged and an open source library called CLINQ. The potential with Rx and IObservable is to move to a much more declarative ViewModel using Subject classes to propagate changed events from the source model through to the View. A conversion from IObservable to the regular databinding interfaces would be needed for the last step.
The problem is that Rx doesn't seem to support the notification that an entity has been removed from the stream. Example below.
The code shows a POCO which uses BehaviorSubject class for the field state. The code goes onto to create a collection of these entities and use Concat to merge the filter streams together. This means that any changes to the POCOs are reported to a single stream.
A filter for this stream is setup to filter for Rating==0. The subscription simply outputs the result to the debug window when an even occurs.
Settings Rating=0 on any element will trigger the event. But setting Rating back to 5 will not see any events.
In the case of CLINQ the output of the query will support INotifyCollectionChanged - so that items added and removed from the query result will fire the correct event to indicate the query result has changed (an item added or removed).
The only way I can think of address this is to set-up two streams with oppossite (dual) queries. An item added to the opposite stream implies removal from the resultset. Failing that, I could just use FromEvent and not make any of the entity models observable - which makes Rx more of just an Event Aggregator. Any pointers?
using System;
using System.ComponentModel;
using System.Linq;
using System.Collections.Generic;
namespace RxTest
{
public class TestEntity : Subject<TestEntity>, INotifyPropertyChanged
{
public IObservable<string> FileObservable { get; set; }
public IObservable<int> RatingObservable { get; set; }
public string File
{
get { return FileObservable.First(); }
set { (FileObservable as IObserver<string>).OnNext(value); }
}
public int Rating
{
get { return RatingObservable.First(); }
set { (RatingObservable as IObserver<int>).OnNext(value); }
}
public event PropertyChangedEventHandler PropertyChanged;
public TestEntity()
{
this.FileObservable = new BehaviorSubject<string>(string.Empty);
this.RatingObservable = new BehaviorSubject<int>(0);
this.FileObservable.Subscribe(f => { OnNotifyPropertyChanged("File"); });
this.RatingObservable.Subscribe(f => { OnNotifyPropertyChanged("Rating"); });
}
private void OnNotifyPropertyChanged(string property)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(property));
// update the class Observable
OnNext(this);
}
}
public class TestModel
{
private List<TestEntity> collection { get; set; }
private IDisposable sub;
public TestModel()
{
this.collection = new List<TestEntity>() {
new TestEntity() { File = "MySong.mp3", Rating = 5 },
new TestEntity() { File = "Heart.mp3", Rating = 5 },
new TestEntity() { File = "KarmaPolice.mp3", Rating = 5 }};
var observableCollection = Observable.Concat<TestEntity>(this.collection.Cast<IObservable<TestEntity>>());
var filteredCollection = from entity in observableCollection
where entity.Rating==0
select entity;
this.sub = filteredCollection.Subscribe(entity =>
{
System.Diagnostics.Debug.WriteLine("Added :" + entity.File);
}
);
this.collection[0].Rating = 0;
this.collection[0].Rating = 5;
}
};
}
Actually I found the Reactive-UI library helpful for this (available in NuGet).
This library includes special IObservable subjects for collections and the facility to create one of these 'ReactiveCollections' over a a traditional INCC collection.
Through this I have streams for new, removed items and changing items in the collection. I then use a Zip to merge the streams together and modify a target ViewModel observable collection. This provides a live projection based on a query on the source model.
The following code solved the problem (this code would be even simpler, but there are some problems with the Silverlight version of Reactive-UI that needed workarounds). The code fires collection changed events by simply adjusting the value of 'Rating' on one of the collection elements:
using System;
using System.ComponentModel;
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using ReactiveUI;
namespace RxTest
{
public class TestEntity : ReactiveObject, INotifyPropertyChanged, INotifyPropertyChanging
{
public string _File;
public int _Rating = 0;
public string File
{
get { return _File; }
set { this.RaiseAndSetIfChanged(x => x.File, value); }
}
public int Rating
{
get { return this._Rating; }
set { this.RaiseAndSetIfChanged(x => x.Rating, value); }
}
public TestEntity()
{
}
}
public class TestModel
{
private IEnumerable<TestEntity> collection { get; set; }
private IDisposable sub;
public TestModel()
{
this.collection = new ObservableCollection<TestEntity>() {
new TestEntity() { File = "MySong.mp3", Rating = 5 },
new TestEntity() { File = "Heart.mp3", Rating = 5 },
new TestEntity() { File = "KarmaPolice.mp3", Rating = 5 }};
var filter = new Func<int, bool>( Rating => (Rating == 0));
var target = new ObservableCollection<TestEntity>();
target.CollectionChanged += new NotifyCollectionChangedEventHandler(target_CollectionChanged);
var react = new ReactiveCollection<TestEntity>(this.collection);
react.ChangeTrackingEnabled = true;
// update the target projection collection if an item is added
react.ItemsAdded.Subscribe( v => { if (filter.Invoke(v.Rating)) target.Add(v); } );
// update the target projection collection if an item is removed (and it was in the target)
react.ItemsRemoved.Subscribe(v => { if (filter.Invoke(v.Rating) && target.Contains(v)) target.Remove(v); });
// track items changed in the collection. Filter only if the property "Rating" changes
var ratingChangingStream = react.ItemChanging.Where(i => i.PropertyName == "Rating").Select(i => new { Rating = i.Sender.Rating, Entity = i.Sender });
var ratingChangedStream = react.ItemChanged.Where(i => i.PropertyName == "Rating").Select(i => new { Rating = i.Sender.Rating, Entity = i.Sender });
// pair the two streams together for before and after the entity has changed. Make changes to the target
Observable.Zip(ratingChangingStream,ratingChangedStream,
(changingItem, changedItem) => new { ChangingRating=(int)changingItem.Rating, ChangedRating=(int)changedItem.Rating, Entity=changedItem.Entity})
.Subscribe(v => {
if (filter.Invoke(v.ChangingRating) && (!filter.Invoke(v.ChangedRating))) target.Remove(v.Entity);
if ((!filter.Invoke(v.ChangingRating)) && filter.Invoke(v.ChangedRating)) target.Add(v.Entity);
});
// should fire CollectionChanged Add in the target view model collection
this.collection.ElementAt(0).Rating = 0;
// should fire CollectionChanged Remove in the target view model collection
this.collection.ElementAt(0).Rating = 5;
}
void target_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine(e.Action);
}
}
}
What's wrong with using an ObservableCollection<T>? Rx is a very easy framework to overuse; I find that if you find yourself fighting against the basic premise of an asynchronous stream, you probably shouldn't be using Rx for that particular problem.
All of the INPC implementations that I've ever seen can be best labeled as shortcuts or hacks. However, I can't really fault the developers since the INPC mechanism that the .NET creators choose to support is terrible. With that said, I have recently discovered, in my opinion, the best implementation of INPC, and the best compliment to any MVVM framework around. In addition to providing dozens of extremely helpful functions and extensions, it also sports the most elegant INPC pattern I've seen. It somewhat resembles the ReactiveUI framework, but it wasn't designed to be a comprehensive MVVM platform. To create a ViewModel that supports INPC, it requires no base class, or interfaces, yes is still able to support complete change notification and Two Way binding, and best of all, all of your properties can be automatic!
It does NOT use a utility such as PostSharp or NotifyPropertyWeaver, but is built around the Reactive Extensions framework. The name of this new framework is ReactiveProperty. I suggest visiting the project site (on codeplex), and pulling down the NuGet package. Also, looks through the source code, because it's really a treat.
I'm in no way associated with the developer, and the project is still fairly new. I'm just really enthusiastic about the features it offers.
To my mind that is not a suitable usage of Rx. An Rx Observable is a stream of 'events' that you can subscribe to. You can react to these events in your View Model, for example adding them to an ObservableCollection which is bound to your view. However, an Observable cannot be used to represent a fixed set of items which you add / remove items from.
The problem is that you are looking at the notifications from a List of TestEntitys, not from the TestEntity themselves. So you see adds, but not changes in any TestEntity. To see this comment out:
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(property));
and you'll see that the program runs the same! Your notifications in your TestEntity's are not wired up to anything. As stated by others, using an ObservableCollection will add this wiring for you.
Related
When my user adds an appointment, he wants to have 2 things happen:
Send an acknowledgement of the appointment to the customer's cell phone, with information about the appointment
24 hours before the appointment, to have a reminder sent out
The main issue is that the event screen displays dates as UTC. This, of course, confuses the customer. So, I need to change the date and format the text message via code.
When I first did this, there was no way to call a custom method from a business event -- so I actually send the message up to Twilio, catch that with a Webhook that sends it into my custom DLL. That massages the message, making everything look right, and then sends it back through Twilio to the customer.
But this is costly (2 messages sent and 1 received for every event) and needlessly complicated. I want to simplify it now, because I have been told there is added functionality in Business events now that allows a call out into custom code. Is this true?
I was told that this would be available starting in 2020 R2. I am looking for it in the docs and training classes, but I can't see anywhere that this is possible.
How do I call custom code from a business event? Can I set up a subscriber that is in a custom DLL?
Is there something that describes this process somewhere? Or did this never make it into the product?
If you're looking to implement a custom Business Event subscriber (coded in your own dll), I know that is possible in 2021 R1.
There are basic instructions in the Release Notes for Developers starting on page 15.
The upshot is you need to reference the PX.BusinessProcess.dll and implement the PX.BusinessProcess.Subscribers.ActionHandlers.IEventAction interface and either of the PX.BusinessProcess.Subscribers.Factories.IBPSubscriberActionHandlerFactory or PX.BusinessProcess.Subscribers.Factories.IBPSubscriberActionHandlerFactoryWithCreateAction interfaces.
Here is an example blog from crestwood to use business events to create an import scenario: https://www.crestwood.com/2020/05/19/using-business-events-to-create-transactions-employee-birthday-checks/
The gist of it would be to create the generic inquiry to monitor. Next, you would create a business event that ties to the generic inquiry. Go under subscribers, select Create New Subscriber, and then name it. It will load the import scenario, and attach the provider to the event.
For provider object in the business event, you can fill from Results or Previous Results.
This is based on the aborted shipments in sales demo, but it shows my custom action after matching shipment number.
once saved, you can see your business event shows up as a subscriber:
Just for the record, the code I was looking for I was in the Acumatica Help files, as TTook suggested. The reason I am publishing more on this is that the answer in help files aren't very reliable (sometimes) down the road, and I wanted a complete version of the code here -- including all of the using/includes. So, here it is:
This creates an event and a subscriber to respond to a Business Event. The code shows writing the screen data to a text file.
using System;
using System.Collections.Generic;
using System.Linq;
using PX.BusinessProcess.Subscribers.ActionHandlers;
using PX.BusinessProcess.Subscribers.Factories;
using PX.BusinessProcess.Event;
using PX.BusinessProcess.DAC;
using PX.BusinessProcess.UI;
using System.Threading;
using PX.Data;
using PX.Common;
using PX.SM;
using System.IO;
using PX.Data.Wiki.Parser;
using PX.PushNotifications;
namespace CustomSubscriber
{
//The custom subscriber that the system executes once the business event
//has occurred
public class CustomSMSEventAction : IEventAction
{
//The GUID that identifies a subscriber
public Guid Id { get; set; }
//The name of the subscriber of the custom type
public string Name { get; protected set; }
//The notification template
private readonly Notification _notificationTemplate;
//The method that writes the body of the notification to a text file
//once the business event has occurred
public void Process(MatchedRow[] eventRows, CancellationToken cancellation)
{
using (StreamWriter file =
new StreamWriter(#"C:\tmp\EventRows.txt"))
{
var graph = PXGenericInqGrph.CreateInstance(
_notificationTemplate.ScreenID);
var parameters = #eventRows.Select(
r => Tuple.Create<IDictionary<string, object>,
IDictionary<string, object>>(
r.NewRow?.ToDictionary(c => c.Key.FieldName, c => c.Value),
r.OldRow?.ToDictionary(c => c.Key.FieldName,
c => (c.Value as ValueWithInternal)?.ExternalValue ??
c.Value))).ToArray();
var body = PXTemplateContentParser.ScriptInstance.Process(
_notificationTemplate.Body, parameters, graph, null);
file.WriteLine(body);
}
}
//The CustomEventAction constructor
public CustomSMSEventAction(Guid id, Notification notification)
{
Id = id;
Name = notification.Name;
_notificationTemplate = notification;
}
}
//The class that creates and executes the custom subscriber
class CustomSubscriberHandlerFactory :
IBPSubscriberActionHandlerFactoryWithCreateAction
{
//The method that creates a subscriber with the specified ID
public IEventAction CreateActionHandler(Guid handlerId,
bool stopOnError, IEventDefinitionsProvider eventDefinitionsProvider)
{
var graph = PXGraph.CreateInstance<PXGraph>();
Notification notification = PXSelect<Notification,
Where<Notification.noteID,
Equal<Required<Notification.noteID>>>>
.Select(graph, handlerId).AsEnumerable().SingleOrDefault();
return new CustomSMSEventAction(handlerId, notification);
}
//The method that retrieves the list of subscribers of the custom type
public IEnumerable<BPHandler> GetHandlers(PXGraph graph)
{
return PXSelect<Notification, Where<Notification.screenID,
Equal<Current<BPEvent.screenID>>,
Or<Current<BPEvent.screenID>, IsNull>>>
.Select(graph).FirstTableItems.Where(c => c != null)
.Select(c => new BPHandler
{
Id = c.NoteID,
Name = c.Name,
Type = LocalizableMessages.CustomNotification
});
}
//The method that performs redirection to the subscriber
public void RedirectToHandler(Guid? handlerId)
{
var notificationMaint =
PXGraph.CreateInstance<SMNotificationMaint>();
notificationMaint.Message.Current =
notificationMaint.Notifications.
Search<Notification.noteID>(handlerId);
PXRedirectHelper.TryRedirect(notificationMaint,
PXRedirectHelper.WindowMode.New);
}
//A string identifier of the subscriber type that is
//exactly four characters long
public string Type
{
get { return "CTTP"; }
}
//A string label of the subscriber type
public string TypeName
{
get { return LocalizableMessages.CustomNotification; }
}
//A string identifier of the action that creates
//a subscriber of the custom type
public string CreateActionName
{
get { return "NewCustomNotification"; }
}
//A string label of the button that creates
//a subscriber of the custom type
public string CreateActionLabel
{
get { return LocalizableMessages.CreateCustomNotification; }
}
//The delegate for the action that creates
//a subscriber of the custom type
public Tuple<PXButtonDelegate, PXEventSubscriberAttribute[]>
getCreateActionDelegate(BusinessProcessEventMaint maintGraph)
{
PXButtonDelegate handler = (PXAdapter adapter) =>
{
if (maintGraph.Events?.Current?.ScreenID == null)
return adapter.Get();
var graph = PXGraph.CreateInstance<SMNotificationMaint>();
var cache = graph.Caches<Notification>();
var notification = (Notification)cache.CreateInstance();
var row = cache.InitNewRow(notification);
row.ScreenID = maintGraph.Events.Current.ScreenID;
cache.Insert(row);
var subscriber = new BPEventSubscriber();
var subscriberRow =
maintGraph.Subscribers.Cache.InitNewRow(subscriber);
subscriberRow.Type = Type;
subscriberRow.HandlerID = row.NoteID;
graph.Caches[typeof(BPEventSubscriber)].Insert(subscriberRow);
PXRedirectHelper.TryRedirect(graph,
PXRedirectHelper.WindowMode.NewWindow);
return adapter.Get();
};
return Tuple.Create(handler,
new PXEventSubscriberAttribute[]
{new PXButtonAttribute {
OnClosingPopup = PXSpecialButtonType.Refresh}});
}
}
//Localizable messages
[PXLocalizable]
public static class LocalizableMessages
{
public const string CustomNotification = "Custom SMS Notification";
public const string CreateCustomNotification = "Custom SMS Notification";
}
}
Ok so I'm fairly new to this. I followed along with this MVVM tutorial from YouTube. It was pretty good and straightforward. Basically it sets up a very basic program with a Model class, DataAcess class, 3 viewmodels (Main window, Employee and ViewModelBase) and finally a view which has a stackpanel and a couple of text boxes that are bound to the FirstName and LastName in the Model.
It all works how it's meant to and I have been through it a number of times and I'm pretty sure I understand how it all works but the trouble that I am having is adding new Employees.
In the DataAccess class (Employee Repository) Employees are added as shown below.
class EmployeeRepository
{
readonly List<Employee> _employee;
public EmployeeRepository()
{
if (_employee == null)
{
_employee = new List<Employee>();
}
_employee.Add(Employee.CreateEmployee("Bob", "Jones"));
_employee.Add(Employee.CreateEmployee("Sarah", "Marshall"));
_employee.Add(Employee.CreateEmployee("Peter", "Piper"));
}
public List<Employee> GetEmployees()
{
return new List<Employee>(_employee);
}
}
And in the Model there is a method call CreateEmployee as such
public class Employee
{
public static Employee CreateEmployee(string firstName, string lastName)
{
return new Employee { FirstName = firstName, LastName = lastName };
}
public string FirstName { get; set; }
public string LastName { get; set; }
}
So I thought I would add a button to the MainWindow and then add another name to the list. Hopping the view would update as an item is updated. Just to see if it would work I just used the code behind.
I thought I could just add a new employee the same way I did in the EmployeeRepository so I tried this
readonly List<Employee> _employee = new List<Employee>();
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
_employee.Add(Employee.CreateEmployee("John", "Smith"));
}
I have tried many many ways of doing this, to no avail. I have watched and read many tutorials and questions, but nothing that I have tried as worked.
What am I missing? I initially thought that it was not working because I am adding the item to the List in the repository, but not to the ObservableCollection that is in the viewmodel. And the AllEmployees ObservableCollection is the ItemSource for view.
readonly EmployeeRepository _employeeRepository;
public ObservableCollection<Model.Employee> AllEmployees
{
get;
private set;
}
public EmployeeListViewModel(EmployeeRepository currentWindowRepository)
{
if (currentWindowRepository == null)
{
throw new ArgumentException("currentWindowRepository");
}
_employeeRepository = currentWindowRepository;
this.AllEmployees = new ObservableCollection<Model.Employee>(_employeeRepository.GetEmployees());
}
But in the button code I tried to implement something similar, but no.
I can also add the view xaml code and MainViewModel codes so that you can see how it's all bound if you like.
Thanks in advance for any help!
You can't do it in "one operation".
When you add a new Employee in the UI, you first need to instantiate your Employee class and add it to the observable collection.
If in valid state, then persist it to in the repository.
private ICommand addEmployeeCommand;
public ICommand AddEmployeeCommand { get { return addEmployeeCommand; } }
public ObservableCollection<Employee> Employees { get; protected set; }
private void AddEmployee()
{
// Get the user input that's bound to the viewmodels properties
var employee = Employee.Create(FirstName, LastName);
// add it to the observable collection
// Note: directly using model in your ViewModel for binding is a pretty bad idea, you should use ViewModels for your Employees too, like:
// Employee.Add(new EmployeeViewModel(employee));
Employees.Add(employee);
// add it to the repository
this.employeeRepository.AddOrUpdate(employee);
}
// in constructor
this.addEmployeeCommand = new DelegateCommand(AddEmployee, CanExecuteAddEmployee);
As noted, avoid directly using your model inside the ViewModel bindings, it has several disadvantages, like you view now depend on your viewmodel. each and every change in the model needs to be reflected in the view, this beats the purpose of a viewmodel which is meant to decouple view, viewmodel and model.
Another disadvantage is, that typically your models are do not implement INotifyPropertyChanged and this will cause memory leaks in the view.
In your EmployeelistViewModel you are creating ObservableCollection , and you think that it will get repopulated automatically upon addition/deletion of employees. secondly in your GetEmployees method you are creating a new list. you should use obser.coll directly in place of List (_employee). And return this ocoll from your method.
One solution to this is to add INPC to your models and to then have your view models watch their models and update themselves accordingly i.e. something like this:
public class MyListType
{
// some data
}
public class MyModel
{
public IList<MyListType> MyListItems { get; set; }
public MyModel()
{
this.MyListItems = new ObservableCollection<MyListType>();
}
}
public class MyListTypeViewModel : ViewModelBase
{
public MyListType Model {get; set;}
// INPC properties go here
}
public class MyViewModel
{
public IList<MyListTypeViewModel> MyListItemViewModels { get; set; }
public MyViewModel(MyModel model)
{
(model.MyListItems as INotifyCollectionChanged).CollectionChanged += OnListChanged;
// todo: create initial view models for any items already in MyListItems
}
private void OnListChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// create any new elements
if (e.NewItems != null)
foreach (MyListType item in e.NewItems)
this.MyListItemViewModels.Add(new MyListTypeViewModel{Model = item});
// remove any new elements
if (e.OldItems != null)
foreach (MyListType item in e.OldItems)
this.MyListItemViewModels.Remove(
this.MyListItemViewModels.First(x => x.Model == item)
);
}
Now your list of view models will automatically stay synched with your list of models. The main problem with this approach is that your models will typically originate from your ORM (database) code, so you will need to work with whatever framework you're using to inject INPC at creation time e.g. if you're using NHibernate then you'll need to use a binding interceptor for INPC and a collection convention to make the lists ObservableCollections.
The exception message is not very helpful but here it is
'observableTeamMember.AssignedTaskEvents.Added' threw an exception of
type 'System.Security.VerificationException'
Method System.Reactive.Linq.Observable.FromEventPattern: type argument
'Bourne.iClean.Planning.Observables.NotifyChildAddedEventArgs`1[T]'
violates the constraint of type parameter 'TEventArgs'.
To give you a brief overview, We have a C#.NET windows service hosted server side which deals with requests from a web application. The service queries a 'planning model' which is a cached version of all the data objects, we maintain this cache in order to speed up responses to queries from the front end.
We are using reactive extensions to keep our data cache up to date whenever certain events occur (like updates to data objects). Some objects in the data cache have child events subscription and when any of these child objects are changed, we update the cache, for example- we have a TeamMemberObject as follows which has some child events associated with it
public interface IObservableTeamMember : IObservableEntity<TeamMember>
{
ChildEvents<TeamCalendar> TeamCalendarEvents {get;}
ChildEvents<AssignedTaskTeamMember> AssignedTaskEvents { get; }
ChildEvents<WorkplaceAssignedTaskTeamMember> WorkplaceAssignedTaskEvents { get; }
}
Whenever any of these child events/objects are updated, we update our cache using the following code. The block below in where I get the security exception
observableTeamMember.AssignedTaskEvents.Added.Subscribe((NotifyChildAddedEventArgs<AssignedTaskTeamMember> e) =>
{
this._addAssignedTaskEntry(e.AddedChild);
});
I am also including partial code for the Child Event Class below
public class ChildEvents<T> : IChildEvents<T>
{
public void Add(T child)
{
var args = new NotifyChildAddedEventArgs<T>(child);
_raiseChildAdded(args);
}
public void Remove(T child)
{
var args = new NotifyChildDeletedEventArgs<T>(child);
_raiseChildDeleted(args);
}
event NotifyChildAddedEventHandler<T> _baseTAdded;
protected void _raiseChildAdded(NotifyChildAddedEventArgs<T> args)
{
var childAdded = _baseTAdded;
if (childAdded != null)
childAdded(this, args);
}
private IObservable<NotifyChildAddedEventArgs<T>> _childAdded;
public IObservable<NotifyChildAddedEventArgs<T>> Added
{
get
{
if (_childAdded == null)
_childAdded = Observable.FromEventPattern<NotifyChildAddedEventHandler<T>, NotifyChildAddedEventArgs<T>>(
(handler) => _baseTAdded += handler,
(handler) => _baseTAdded -= handler)
.Select(e => e.EventArgs);
return _childAdded;
}
}
}
This exception only happens occasionally and we are a team of 3 and it has happened randomly to all of us at some point. We use reactive extensions throughout this project and we are unable to explain the cause. Any help is greatly appreciated.
Thanks
Tapashya
That error suggests that your Bourne.iClean.Planning.Observables.NotifyChildAddedEventArgs<T> does not inherit from System.EventArgs, which is required by Observable.FromEventPattern since it specifically expects you to confirm to the standard .NET event pattern.
Let us say I have this ReactiveUI view model structure, with Model being some arbitrary model type.
class ViewModel : ReactiveObject
{
private readonly ReactiveList<Model> _models;
public IReactiveList<Model> Models { get { return _models; } }
public IReactiveCommand LoadModels { get; private set; }
public bool LoadingModels { get; private set; } // Notifies;
}
And these models come from a task-based asynchronous API modeled by this interface:
interface ITaskApi
{
Task<IEnumerable<Model>> GetModelsAsync();
}
After taking a look at how Octokit.net's reactive library was written, I wrote the following class to adapt the API to a reactive world:
class ObservableApi
{
private readonly ITaskApi _taskApi;
public IObservable<Model> GetModels() {
return _taskApi.GetModelsAsync().ToObservable().SelectMany(c => c);
}
}
And now, I have written the following ways to implement loading of models inside of the the LoadModels command, in the ViewModel() constructor:
// In both cases, we want the command to be disabled when loading:
LoadModels = new ReactiveCommand(this.WhenAny(x => x.LoadingModels, x => !x.Value));
// First method, with the Observable API;
LoadModels.Subscribe(_ =>
{
LoadingModels = true;
_models.Clear();
observableClient.GetModels().ObserveOnDispatcher()
.Subscribe(
m => _models.Add(m),
onCompleted: () => { LoadingModels = false; });
});
// Second method, with the task API;
LoadModels.Subscribe(async _ =>
{
LoadingModels = true;
try {
var loadedModels = await taskClient.GetModelsAsync();
_models.Clear();
_models.AddRange(loadedModels);
} catch (Exception ex) {
RxApp.DefaultExceptionHandler.OnNext(ex);
} finally {
LoadingModels = false;
}
});
While I think that both ways will do the job, I have the following misgivings:
In the first sample, should I dispose the inner subscription or will that be done when the inner observable completes or errors?
In the second sample, I know that exceptions raised in the GetModelsAsync method will be swallowed.
What is the "best", most idiomatic way to populate a ReactiveList<T> from an asynchronous enumeration (either IObservable<T> or Task<IEnumerable<T>> (or would IObservable<IEnumerable<T>> be better?))?
After taking a look at how Octokit.net's reactive library was written, I wrote the following class to adapt the API to a reactive world:
While you sometimes want to do this (i.e. flatten the collection), it's usually more convenient to just leave it as IEnumerable<T> unless you then plan to invoke an async method on each item in the list. Since we just want to stuff everything in a List, we don't want to do this. Just leave it as Task<T>
In the first sample, should I dispose the inner subscription or will that be done when the inner observable completes or errors?
Any time you have a Subscribe inside another Subscribe, you probably instead want the SelectMany operator. However, there is a better way to do what you're trying to do, you should check out this docs article for more info.
So, here's how I would write your code:
// In both cases, we want the command to be disabled when loading:
LoadModels = new ReactiveCommand();
LoadModels.RegisterAsyncTask(_ => taskClient.GetModelsAsync())
.Subscribe(items =>
{
// This Using makes it so the UI only looks at the collection
// once we're totally done updating it, since we're basically
// changing it completely.
using (_models.SuppressChangeNotifications())
{
_models.Clear();
_models.AddRange(items);
}
});
LoadModels.ThrownExceptions
.Subscribe(ex => Console.WriteLine("GetModelsAsync blew up: " + ex.ToString());
// NB: _loadingModels is an ObservableAsPropertyHelper<bool>
LoadModels.IsExecuting
.ToProperty(this, x => x.LoadingModels, out _loadingModels);
For the past couple of weeks I've been working on developing a cross platform app (IOS/Android/WP7) using the MVVMCross framework. Today I ran into a problem I don't really know how to solve, so hopefully you can push me in the right direction.
In the IOS I have the following construction for navigating to another page (the code below is located in a ViewModel):
KeyValuePair<string,string> kvpAct1 = new KeyValuePair<string, string>("short", ".countertest5");
public IMvxCommand BeckhoffActuator1
{
get
{
return new MvxRelayCommand<Type>((type) => this.RequestNavigate<Beckhoff.BeckhoffActuatorViewModel>(kvpAct1));
}
}
When this IMvxCommand is fired (button pressed) the next View is loaded, in this case the BeckhoffActuatorViewModel. In the code of the BeckhoffActuatorView I use the keyvaluepair from above:
public class BeckhoffActuatorView : MvxTouchDialogViewController<BeckhoffActuatorViewModel>
{
ICollection<string> icol;
public BeckhoffActuatorView(MvxShowViewModelRequest request) : base(request, UITableViewStyle.Grouped, null, true)
{
icol = request.ParameterValues.Values;
}
public override void ViewDidLoad()
{
//Code
}
}
This construction is working fine in IOS, but I would like to use the same construction in my android App.
The code in the ViewModel hasn't changed since that's the whole idea of MVVM. But the code of the BackhoffActuatorView is different for Android:
public class BeckhoffActuatorView : MvxBindingActivityView<BeckhoffSensorViewModel>
{
public ICollection<string> icol;
public BeckhoffActuatorView()
{
Debug.WriteLine("Standard");
}
public BeckhoffActuatorView(MvxShowViewModelRequest request)
{
Debug.WriteLine("Custom");
icol = request.ParameterValues.Values;
}
protected override void OnViewModelSet()
{
SetContentView(Resource.Layout.BeckhoffActuatorView);
}
}
The code above isn't working, the MvxBindingActivityView doesn't seem to implement something similar to the ViewController I use in IOS. The code only come in the standard constructor, and when I leave that one out completely it won't compile/run.
Does anyone know know I can access the keyvaluepair I send with the RequestNavigate? Thank you!
MVVMCross is very convention based - and it works on the idea of passing messages between ViewModels wherever possible.
If you navigate to a ViewModel using:
KeyValuePair<string,string> kvpAct1 = new KeyValuePair<string, string>("short", ".countertest5");
public IMvxCommand BeckhoffActuator1
{
get
{
return new MvxRelayCommand<Type>((type) => this.RequestNavigate<Beckhoff.BeckhoffActuatorViewModel>(kvpAct1));
}
}
then you should be able to pick that up in the BeckhoffActuatorViewModel using the constructor:
public class BeckhoffActuatorViewModel : MvxViewModel
{
public BeckhoffActuatorViewModel(string short)
{
ShortValue = short;
}
private string _shortValue;
public string ShortValue
{
get
{
return _shortValue;
}
set
{
_shortValue = value;
FirePropertyChanged("ShortValue");
}
}
}
And your views can then access ViewModel.ShortValue (for iOS this can be done after base.ViewDidLoad(), for Android after OnCreate() and for WP7 after OnNavigatedTo)
For an example of this, take a look at the TwitterSearch example:
https://github.com/slodge/MvvmCrossTwitterSearch
This has a HomeViewModel which calls navigate using:
private void DoSearch()
{
RequestNavigate<TwitterViewModel>(new { searchTerm = SearchText });
}
and a TwitterViewModel which receives the searchTerm using the constructor:
public TwitterViewModel(string searchTerm)
{
StartSearch(searchTerm);
}
Please note that only strings are allowed in this message passing at present - but you can always serialise your own objects using JSON.Net - or you can extend the framework - it's open source.
Please note that only strings, ints, doubles and bools are allowed in this constructor parameter passing at present - this is due to serialisation requirements for Xaml Urls and for Android Intents. If you want to experiment with navigation using your own custom serialised objects, then please see http://slodge.blogspot.co.uk/2013/01/navigating-between-viewmodels-by-more.html.
Also, note that if you want to use the anonymous object navigation (RequestNavigate<TwitterViewModel>(new { searchTerm = SearchText });) then you will need to make sure that an InternalsVisibleTo attribute is set - see https://github.com/slodge/MvvmCrossTwitterSearch/blob/master/TwitterSearch.Core/Properties/AssemblyInfo.cs:
[assembly: InternalsVisibleTo("Cirrious.MvvmCross")]
Further... not for the faint-hearted... and this isn't "good mvvm code"... but if you really want/need to access the MvxShowViewModelRequest data inside an Android activity, then you can extract it from the incoming Intent - there's an Extras string containing the request (see the deserialisation in CreateViewModelFromIntent in https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross/Android/Views/MvxAndroidViewsContainer.cs)