I recently started to use OAPH in my ViewModels.
To improve performance I'm using deferred subscription.
Given the following model class:
public class Model : ReactiveObject
{
[ReactiveUI.Fody.Helpers.Reactive]
public string Value { get; set; }
}
And the view model:
public class ViewModel : ReactiveObject
{
private readonly Model _model;
private readonly ObservableAsPropertyHelper<string> _propertyOAPH;
public ViewModel(Model model)
{
this._model = model;
this.WhenAnyValue(x => x._model.Value)
.ToProperty(this, nameof(this.Property), out this._propertyOAPH, deferSubscription: true, scheduler: RxApp.MainThreadScheduler);
}
public string Property => this._propertyOAPH.Value;
}
The first time view model's property will be accessed, the source observable will be subscribed and the default OAPH value will be returned.
As in my example I'm using the WhenAnyValue extension method, the resulting observable will start with a value.
Once this value is observed by the OAPH, the ViewModel will emit a PropertyChanged event.
var model = new Model { Value = "value" };
new ViewModel(model)
.WhenAnyValue(x => x.Property)
.Select(p => p ?? "null")
.Subscribe(p => Console.WriteLine($" - ViewModel property: {p}"));
/**
* - ViewModel property: null
* - ViewModel property: value
*/
Is there a way for the OAPH initial value to be the first value of the source observable?
I understand that from a pure reactive perspective having the initial value temporarily null or wrong (if OAPH's initial value is set) is not an issue. But on iOS, TableViewCell height is calculated according to its content when initialized (if auto layout is used). TableViewVell height is not updated afterward.
I'm investigating first if there is an easy way to "solve" this problem with reactiveUI. If there is none, I'll investigate on a pure iOS solution.
Notes:
ViewModel's property must be observed on the main thread as it will trigger UI changes
Setting a default value to the OAPH is not an option as the source observable first value (Model property value) may change before subscription.
I was able to get this to work.
ViewModel.cs
public ViewModel(Model model)
{
this._model = model;
Observable.Defer(() => this.WhenAnyValue(x => x._model.Value)).ToProperty(this, nameof(this.Property), out this._propertyOAPH, deferSubscription: true, scheduler: RxApp.MainThreadScheduler); }
I use Observable.Defer( will defer the evaluation of the property until the observable is subscribed.
Related
Given I have the following setup (simplified version, removed logic to add to parent view and constraints etc).
public class TestViewModel : MvxViewModel
{
string _text;
public string Text
{
get => _text;
set
{
_text = value;
RaisePropertyChanged(() => Text);
}
}
}
public class TestViewController : MvxViewController<TestViewModel>
{
CustomViewA customViewA;
public TestViewController()
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
var bindingSet = this.CreateBindingSet<TestViewController, TestViewModel>();
bindingSet
.Bind(customViewA)
.For(v => v.Text)
.To(vm => vm.Text);
bindingSet.Apply();
}
}
public class CustomViewA : UIView
{
CustomViewB customViewB;
public string Text
{
get => customViewB.Text;
set => customViewB.Text = value;
}
}
public class CustomViewB : UIView
{
UITextField textField;
public string Text
{
get => textField.Text;
set => textField.Text = value;
}
}
Why is it that the bindings do not work? Only if I would make the UITextField in CustomViewB public and directly bind to it in the ViewController rather than the public property that directs to the Text property it seems to work. Like so:
bindingSet
.Bind(customViewA.customViewB.textField)
.For(v => v.Text)
.To(vm => vm.Text);
What am I missing here?
It depends on the requirements you have.
Binding in one direction should work (view model-to-view), I have tested your code and when the ViewModel property changes, the change is propagated to CustomViewA and from there to CusomViewB and finally to the UITextField.
However, the problem is with the opposite direction (view-to-view model). When the user updates the text field, its Text property changes. However, there is nothing notified about this change.
Although the property Text points to the text field, it is not "bound" to it, so when TextField's Text changes, the property itself doesn't know about it and neither does the MvvmCross binding.
In fact, MvvmCross binding in the control-to-view model direction is based on the ability to observe an event that tells the binding to check the new value of the bining source. This is already implemented for UITextField's Text, and it hooks up the EditingChanged event (see source code).
You can still make custom bindings work in the view-to-view model direction by implementing them manually. This is described in the documentation.
I have a button in View "A" which already has a bindingSet attached to it (it binds to ViewModel "A"). I have button though which needs to be bound to ViewModel "B".
What is the best way to do this?
Your ViewModel is your Model for your View.
If that ViewModel is made up of parts, then that can be done by aggregation - by having your ViewModel made up of lots of sub-models - e.g:
// simplified pseudo-code (add INPC to taste)
public class MyViewModel
{
public MainPartViewModel A {get;set;}
public SubPartViewModel B {get;set;}
public string Direct {get;set;}
}
With this done, then a view component can be bound to direct sub properties as well as sub properties of sub view models:
set.Bind(button).For("Title").To(vm => vm.Direct);
set.Bind(button).For("TouchUpInside").To(vm => vm.A.GoCommand);
set.Bind(button).For("Hidden").To(vm => vm.B.ShouldHideThings);
As long as each part supports INotifyPropertyChanged then data-binding should "just work" in this situation.
If that approach doesn't work for you... In mvvmcross, you could set up a nested class within the View that implemented IMvxBindingContextOwner and which provided a secondary binding context for your View... something like:
public sealed class Nested : IMvxBindingContextOwner, IDisposable {
public Nested() { _bindingContext = new MvxBindingContext(); }
public void Dispose() {
_bindingContext.Dispose();
}
private MvxBindingContext _bindingContext;
public IMvxBindingContext BindingContext { get { return _bindingContext; } }
public Thing ViewModel {
get { return (Thing)_bindingContext.DataContext; }
set { _bindingContext.DataContext = value; }
}
}
This could then be used as something like:
_myNested = new Nested();
_myNested.ViewModel = /* set the "B" ViewModel here */
var set2 = _myNested.CreateBindingSet<Nested, Thing>();
// make calls to set2.Bind() here
set2.Apply();
Notes:
I've not run this pseudo-code, but it feels like it should work...
to get this fully working, you will also want to call Dispose on the Nested when Dispose is fired on your View
given that Views and ViewModels are normally written 1:1 I think this approach is probably going to be harder to code and to understand later.
I'm new in the ASP.NET Framework, I've read the fundamental and have some understanding(theory) on the framework but not much in practice.
I'm struggling with the dropdownlistfor helper method, it comes down to having a weird behavior when i attempt to change the value of the selected item programatically.
In my controller i have the Index action method that receives a parameter of type Tshirt, inside this action method i set the property named Color of the Tshirt object with a value of 2.
In the view (strongly typed) i have a list of colors and pass this list as an argument for the constructor of the ColorViewModel class that will be in charge of returning SelectListItems for my list of colors.
In the view I then set the Selected property of my ColorViewModel object with the value coming from model.Color, now i have set everything so that when i call
#Html.DropDownListFor(x => x.Color, cvm.SelectItems, "Select a color")
I will have my dropdown displayed with the selected item.
When the request(GET) is performed the page is rendered by the browser and the dropdownlist appears with the correct value selected that was established with the value 2 in the Index action method, the dropdown displays "Blue" this is correct.
Then if i select a different item in the dropdownlist (RED, having an id of one) and submit the form, the Save action method is called and i know that the model is reaching the action method with a model.Color=1, which is correct.
Then i decide to redirect to the index action method passing the model object, the index action method changes the Color property back to 2, so when the page is rendered it should display again the value of Blue in the dropdown, but it doesn't, it displays Red.
if you comment out the following line in the Save action method you will get a different behavior.
//tshirt.Color = 3;
i know this logic im following doesnt make much sense from a bussines logic perspective, im just trying to understand what i am doing wrong to not get the expected result.
Given the following model
public class Color
{
public int Id { get; set; }
public string Description { get; set; }
}
I Create the following view model
public class ColorViewModel
{
public int Selected { get; set; }
private List<Color> Colors;
public IEnumerable<SelectListItem> SelectItems { get { return new SelectList(this.Colors, "Id", "Description", this.Selected); } }
private ColorViewModel() { }
public ColorViewModel(List<Color> colors)
{
this.Colors = colors;
}
}
This is my Controller
public class HomeController : Controller
{
[HttpGet()]
public ActionResult Index(Tshirt tshirt)
{
tshirt.Color = 2;
Tshirt t = new Tshirt();
t.Color = tshirt.Color;
return View(t);
}
[HttpPost()]
public ActionResult Save(Tshirt tshirt)
{
//tshirt.Color = 3;
return RedirectToAction("Index", tshirt);
//return View("Index",tshirt);
}
}
And Finally my View
#{
List<Color> colors = new List<Color>(){
new Color(){Id=1, Description="Red"},
new Color(){Id=2, Description="Blue"},
new Color(){Id=3, Description="Green"}
};
ColorViewModel cvm = new ColorViewModel(colors) { Selected = Model.Color };
}
#using(#Html.BeginForm("Save","Home")){
#Html.DropDownListFor(x => x.Color, cvm.SelectItems, "Select a color")
<input type="submit" />
}
I have uploaded the complete code: VS Solution
Because when you redirect, you are passing the updated model
return RedirectToAction("Index", tshirt);
I have an "Embedded Resource" view. In this view I am using the following model
public class TestModel
{
public TestModel()
{
CustomModel1 = new CustomModel1 ();
CustomModel2 = new CustomModel2();
}
public CustomModel1 CustomModel1 { get; set; }
public CustomModel2 CustomModel2{ get; set; }
}
In this view I have a form and inside it I am using #Html.EditorFor instead of #Html.Partial, because when I use #Html.Partial the CustomModel1 passed to the action (when the form is submitted) is empty.
#Html.EditorFor(m => m.CustomModel1, Constants.CustomEmbeddedView1)
However when I use #Html.EditorFor and pass as a template a "Content" view
#Html.EditorFor(m => m.CustomModel1, "~/Views/Common/_CustomPartialView.cshtml")
I get the following error:
The model item passed into the dictionary is null, but this dictionary requires a non-null model item of type 'System.Int32'.
If I set the "Content" view to be an "Embedded Resource" everything works fine.
Is there any way to resolve this problem? Perhaps there is another solution to the model binding problem instead of using #Html.EditorFor.
I found a solution to my problem. I still do not know why the error is thrown, but at least I fixed the model binding.
The problem with the model binding, is that when you call #Html.Partial
#Html.Partial("~/Views/Common/_CustomPartialView.cshtml", Model.CustomModel1)
The elements that are dispayed (I use #Html.EditorFor(m => m.Name) for example in the partial view) have an id="Name". So the model binding tries to find a "Name" property inside the TestModel, but the name property is inside the CustomModel1 property. This is why the model binding does not work, and the Name property is an empty string when the form is submitted.
The fix is to set the HtmlFieldPrefix.
var dataDictCustomModel1 = new ViewDataDictionary { TemplateInfo = { HtmlFieldPrefix = "CustomModel1" } };
#Html.Partial("~/Views/Common/_CustomPartialView.cshtml", Model.CustomModel1, dataDictCustomModel1 )
This way the id of the Name property becomes id="CustomModel1_Name", thus allowing the model binder to properly set the value of the Name property.
There may be a better solution for this, but so far this is the best, that I have come up with.
My current implementation for service and business layer is straight forward as below.
public class MyEntity { }
// Business layer
public interface IBusiness { IList<MyEntity> GetEntities(); }
public class MyBusinessOne : IBusiness
{
public IList<MyEntity> GetEntities()
{
return new List<MyEntity>();
}
}
//factory
public static class Factory
{
public static T Create<T>() where T : class
{
return new MyBusinessOne() as T; // returns instance based on T
}
}
//Service layer
public class MyService
{
public IList<MyEntity> GetEntities()
{
return Factory.Create<IBusiness>().GetEntities();
}
}
We needed some changes in current implementation. Reason being data grew over the time and service & client cannot handle the volume of data. we needed to implement pagination to the current service. We also expect some more features (like return fault when data is more that threshold, apply filters etc), so the design needs to be updated.
Following is my new proposal.
public interface IBusiness
{
IList<MyEntity> GetEntities();
}
public interface IBehavior
{
IEnumerable<T> Apply<T>(IEnumerable<T> data);
}
public abstract class MyBusiness
{
protected List<IBehavior> Behaviors = new List<IBehavior>();
public void AddBehavior(IBehavior behavior)
{
Behaviors.Add(behavior);
}
}
public class PaginationBehavior : IBehavior
{
public int PageSize = 10;
public int PageNumber = 2;
public IEnumerable<T> Apply<T>(IEnumerable<T> data)
{
//apply behavior here
return data
.Skip(PageNumber * PageSize)
.Take(PageSize);
}
}
public class MyEntity { }
public class MyBusinessOne : MyBusiness, IBusiness
{
public IList<MyEntity> GetEntities()
{
IEnumerable<MyEntity> result = new List<MyEntity>();
this.Behaviors.ForEach(rs =>
{
result = rs.Apply<MyEntity>(result);
});
return result.ToList();
}
}
public static class Factory
{
public static T Create<T>(List<IBehavior> behaviors) where T : class
{
// returns instance based on T
var instance = new MyBusinessOne();
behaviors.ForEach(rs => instance.AddBehavior(rs));
return instance as T;
}
}
public class MyService
{
public IList<MyEntity> GetEntities(int currentPage)
{
List<IBehavior> behaviors = new List<IBehavior>() {
new PaginationBehavior() { PageNumber = currentPage, }
};
return Factory.Create<IBusiness>(behaviors).GetEntities();
}
}
Experts please suggest me if my implementation is correct or I am over killing it. If it correct what design pattern it is - Decorator or Visitor.
Also my service returns JSON string. How can I use this behavior collections to serialize only selected properties rather than entire entity. List of properties comes from user as request. (Kind of column picker)
Looks like I don't have enough points to comment on your question. So, I am gonna make some assumption as I am not a C# expert.
Assumption 1: Looks like you are getting the data first and then applying the pagination using behavior object. If so, this is a wrong approach. Lets say there are 500 records and you are showing 50 records per fetch. Instead of simply fetching 50 records from DB, you are fetching 500 records for 10 times and on top of it you are adding a costly filter. DB is better equipped to do this job that C# or Java.
I would not consider pagination as a behavior with respect to the service. Its the behavior of the presentation layer. Your service should only worry about 'Data Granularity'. Looks like one of your customer wants all the data in one go and others might want a subset of that data.
Option 1: In DAO layer, have two methods: one for pagination and other for regular fetch. Based on the incoming params decide which method to call.
Option 2: Create two methods at service level. One for a small subset of data and the other for the whole set of data. Since you said JSON, this should be Restful service. Then based on the incoming URL, properly call the correct method. If you use Jersey, this should be easy.
In a service, new behaviors can be added by simply exposing new methods or adding new params to existing methods/functionalities (just make sure those changes are backward compatible). We really don't need Decorator or Visitor pattern. The only concern is no existing user should be affected.