I'm wondering if I'm doing this the correct way - this method works, but feels somewhat 'dirty'. Essentially, a button in an MvxTableViewCell changes a parameter of the bound object, but the cell does not update to reflect the change until it's scrolled out of view and back into view (ie the cell is 'redrawn'). All the examples here are simplified, but you get the idea..
Firstly, my object:
public class Expense
{
public decimal Amount { get; set; }
public bool Selected { get; set; }
public Command FlipSelected
{
get { return new MvxCommand(()=> this.Selected = !this.Selected); }
}
}
Secondly, my cell (in the constructor) contains:
this.DelayBind(() =>
{
var set = this.CreateBindingSet<HistoryCell, Expense>();
set.Bind(this.TitleText).To(x => x.Amount);
set.Bind(this.SelectButton).To(x=> x.FlipSelected);
set.Bind(this.SelectButton).For(x => x.BackgroundColor).To(x => x.Selected).WithConversion(new ButtonConverter(), null);
set.Apply();
});
And i have a valueconverter that returns the background colour of the button:
class ButtonConverter : MvxValueConverter<bool, UIColor>
{
UIColor selectedColour = UIColor.FromRGB(128, 128, 128);
UIColor unSelectedColour = UIColor.GroupTableViewBackgroundColor;
protected override UIColor Convert(bool value, Type targetType, object parameter, CultureInfo culture)
{
return value ? selectedColour : unSelectedColour;
}
protected override bool ConvertBack(UIColor value, Type targetType, object parameter, CultureInfo culture)
{
return value == selectedColour;
}
}
Right, so what happens is, if i click the button in the cell, it runs the command that flips the bool value Selected, which in turn binds back to the background colour of the cell via the ButtonConverter value converter.
The problem I'm having is that the cell doesn't update straight away - only when I scroll out of view of that cell and back into view (ie the cell is redrawn). So i thought I'd just cause the cell to become 'dirty':
this.SelectButton.TouchUpInside += (o, e) =>
{
this.SetNeedsDisplay();
};
But this doesn't work. What does work is putting additional code inside the TouchUpInside event that manually changes the background colour. But I'm assuming this isn't the correct way of doing it.
Do I need to trigger RaisePropertyChanged when I change the value of Selected in the Expense object? How can I do that when it's just an object?
Really hoping Stuart can help out on this one ;)
I think your analysis is correct - the UI isn't updating live because there are no change messages from your Expense objects.
To provide 'traditional' change notifications in your view model objects, you need to make sure each one supports INotifyPropertyChanged. This small interface is easy to implement yourself if you want to - or you can modify your Expense to inherit the built-in MvxNotifyPropertyChanged helper class if you prefer - then RaisePropertyChanged would be available.
As one other alternative, you can also implement the new 'Rio' field based binding if you prefer. For an intro to this, see N=36 in http://mvvmcross.blogspot.com
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 number of ListBoxFor elements on a form in edit mode. If there was data recorded in the field then the previously selected items are displaying correctly when the form opens. If the field is empty though an error is thrown as the items parameter cannot be null. Is there a way to check in the view and if there is data to use the ListBoxFor with the four parameters but if there isn't to only use three parameters, leaving out the selected items?
This is how I'm declaring the ListBoxFor:
#Html.ListBoxFor(model => model.IfQualityPoor, new MultiSelectList(ViewBag.IfPoor, "Value", "Text", ViewBag.IfQualityPoorSelected), new { #class = "chosen", multiple = "multiple" })
I'm using the ViewBag to pass the ICollection which holds the selected items as the controller then joins or splits the strings for binding to the model field. The MultiSelectLists always prove problematic for me.
Your question isn't entirely clear, but you're making it way harder on yourself than it needs to be using ListBoxFor. All you need for either DropDownListFor or ListBoxFor is an IEnumerable<SelectListItem>. Razor will take care of selecting any appropriate values based on the ModelState.
So, assuming ViewBag.IfPoor is IEnumerable<SelectListItem>, all you need in your view is:
#Html.ListBoxFor(m => m.IfQualityPoor, (IEnumerable<SelectListItem>)ViewBag.IfPoor, new { #class = "chosen" })
The correct options will be marked as selected based on the value of IfQualityPoor on your model, as they should be. Also, it's unnecessary to pass multiple = "multiple" in in your htmlAttributes param, as you get that just by using ListBoxFor rather than DropDownListFor.
It's even better if you use a view model and then add your options as a property. Then, you don't have to worry about casting in the view, which is always a good way to introduce runtime exceptions. For example:
public class FooViewModel
{
...
public IEnumerable<SelectListItem> IfQualityPoorOptions { get; set; }
}
Then, you set this in your action, before returning the view (instead of setting ViewBag). Finally, in your view:
#Html.ListBoxFor(m => m.IfQualityPoor, Model.IfQualityPoorOptions, new { #class = "chosen" })
Much simpler, and you'll never have any issues doing it that way.
UPDATE (based on comment)
The best way to handle flattening a list into a string for database storage is to use a special property for that, and then custom getter and setter to map to/from. For example:
public string IfQualityPoor
{
get { return IfQualityPoorList != null ? String.Join(",", IfQualityPoorList) : null; }
set { IfQualityPoorList = !String.IsNullOrWhiteSpace(value) ? value.Split(',').ToList() : null; }
}
[NotMapped]
public List<string> IfQualityPoorList { get; set; }
Then, you post to/interact with IfQualityPoorList, and the correct string will be set in the database automatically when you save.
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);
First time using MvvmCross Value Convertors. I've created a value convertor to handle data manipulation between a bool? element in my view and a bool property in my view model.
public sealed class NullableBooleanValueConverter : MvxValueConverter<bool, bool?>
{
// ViewModel -> View
protected override bool? Convert(bool value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (bool?)value;
}
// View -> ViewModel
protected override bool ConvertBack(bool? value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value ?? false;
}
}
And I've bound my view element to the view model when in my view's ViewDidLoad.
var set = this.CreateBindingSet<SettingsView, SettingsViewModel>();
set.Bind(wifiOnlyElement).For(View => View.BooleanValue).To(ViewModel => ViewModel.ConnectOnWifiOnly).WithConversion("NullableBoolean").TwoWay();
set.Apply();
Note: I added the .TwoWay() binding modifier to the set.Bind, thinking the mode might have to be explicitly stated, with no change.
When the view appears the Convert method of NullableBooleanValueConverter is called.
However, when the view is closed, the corresponding ConvertBack method is not called.
Based on this question I suspect that I'm not binding to the correct property. The screen control that I'm creating the binding on is a custom UITableViewCell descended from Xamarin's Dialog/Element classes and the nullable BooleanValue on this screen control is public and that's where the true/false value is stored when the element is tapped.
BooleanValue is a public property on a Dialog/Element class called CheckboxElement that inherits from the base Element class.
NSObject
|_ Element
|_ CheckboxElement
.BooleanValue
There is an overridden property in CheckboxElement called Selected that changes the value of BooleanValue when the element is tapped.
public override void Selected(DialogViewController dvc, UITableView tableView, NSIndexPath indexPath)
{
BooleanValue = !BooleanValue;
...
}
Selected is called by the DialogViewController's RowSelected method.
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
Container.Selected(indexPath);
}
Which calls the DialogViewController's Selected method (element is the CheckboxElement).
public virtual void Selected(NSIndexPath indexPath)
{
var section = Root.Sections[indexPath.Section];
var element = section.Elements[indexPath.Row];
ActiveElement = element;
element.Selected(this, Root.TableView, indexPath);
}
Doesn't look like there's anything there that's interfering.
Let me know if you need more info. Thanks in advance.
Mvvmcross relies on event notifications to tell it when values have changed.
When event notifications aren't available, then you can write custom bindings to help MvvmCross know when the ui has updated. For more on this, see n=28 - custom,bindings in http://mvvmcross.blogspot.com
For the specific case of Monotouch.Dialog, Mvvmcross provides its own branch which includes two-way bindings for boolean elements such as UiSwitch-based Elements. You may find it easier to use this mvvmcross branch - for more on this, look for dialog in the n+1 videos.
I am trying to figure out how to:
1. use IB, in Xcode 4+ to visually create a custom subclass of UITableViewCell to use in MT.
How to use that custom class as an element in MT.Dialog.
I have searched extensively and haven't found any example or been able to solve it.
Here is the process I have been trying:
Step 1 seems easy enough now that I have found a good tutorial: http://www.arcticmill.com/2012/05/uitableview-with-custom-uitableviewcell.html
Step 2 seems to be where I am stuck. Once I have the new class, with a few labels dropped onto it in this case:
public partial class CustomListCell : UITableViewCell {
public CustomListCell () :base(UITableViewCellStyle.Default,"CellID") {
}
public void UpDateData(string lbl1, string lbl2, string lbl3) {
this.lblLabel1.Text = lbl1;
this.lblLabel2.Text = lbl2;
this.lblLabel3.Text = lbl3;
}
}
I cannot figure out how to turn it into something I can use in MT.Dialog. I have tried :
public partial class CustomListCell :Element
but the label controls don't seem to every be created.No matter where I put a call to UpdateData they are all null, hence a null reference exception, even if the constructor has executed just fine. I've also tried making it an OwnerDrawnElement, but ran into a couple of problems with that.
Is this possible? Is there a recommended pattern?
I think the sample you are looking for is the OwnerDrawnCell: https://github.com/migueldeicaza/MonoTouch.Dialog/blob/master/MonoTouch.Dialog/Elements/OwnerDrawnElement.cs
See how it overrides the GetCell() method to provide a custom cell:
public override UITableViewCell GetCell (UITableView tv)
{
OwnerDrawnCell cell = tv.DequeueReusableCell(this.CellReuseIdentifier) as OwnerDrawnCell;
if (cell == null)
{
cell = new OwnerDrawnCell(this, this.Style, this.CellReuseIdentifier);
}
else
{
cell.Element = this;
}
cell.Update();
return cell;
}
You just need to do the same thing - except you need to replace OwnerDrawnCell with the XIB-loaded cell.
I've also done a blog post on how I load cells from XIBs using the new iOS6 variant of the DequeueReusableCell API - see http://slodge.blogspot.co.uk/2013/01/uitableviewcell-using-xib-editor.html