Text on TextBox with UpdateSourceTrigger=PropertyChanged is not updated when coercion of text input results in unchanged source value - textbox

I have a text box whose Text property has a TwoWay MultiBinding with UpdateSourceTrigger set to PropertyChanged. The first Binding is to a dependency property (Value) which has a PropertyChangedCallBack function that rounds the value to one decimal place.
The purpose of the text box is to perform the rounding as the user types rather than when the text box loses focus, hence why UpdateSourceTrigger is set to PropertyChanged.
The problem I am having is that if text is entered that does NOT result in Value changing, the Text property and Value become out of sync. Only if the rounding operation causes Value to change does Text get updated on the fly. E.g., if Text and Value are both 123.4 and the user types 1 after this, Value is rounded to the same value (123.4), but Text shows 123.41. However, if 9 is then typed after the 4, Value is rounded up to 123.5. And because of this actual change, Text is then updated to the same (123.5).
Is there any way of forcing a text box to update from its source even when the source hasn't changed since the last trigger? I have tried using BindingExpressionBase.UpdateTarget() but this only works when UpdateSourceTrigger is set to Explicit, which can't be used as Value no longer gets updated prior to a suitable time where UpdateTarget could be called (such as a TextInput handler). I have tried other methods such as explicitly updating the Text value from the bound Value, forcing an actual change to Value temporarily to invoke an update, but these "hacks" either don't work or cause other problems.
Any help would be greatly appreciated.
The code is below.
XAML snippet
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{local:NumberFormatConverter}"
UpdateSourceTrigger="Explicit"
Mode="TwoWay">
<Binding Path="Value"
RelativeSource="{RelativeSource AncestorType={x:Type Window}}"
Mode="TwoWay" />
</MultiBinding>
</TextBox.Text>
</TextBox>
C# snippet
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value", typeof(decimal), typeof(MainWindow),
new FrameworkPropertyMetadata(0m,
new PropertyChangedCallback(OnValueChanged)));
private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
obj.SetValue(ValueProperty, Math.Round((decimal)args.NewValue, 1));
}
Converter class required
public class NumberFormatConverter : MarkupExtension, IMultiValueConverter
{
public static NumberFormatConverter Instance { private set; get; }
static NumberFormatConverter()
{
Instance = new NumberFormatConverter();
}
public override object ProvideValue(IServiceProvider serviceProvider_)
{
return Instance;
}
#region Implementation of IMultiValueConverter
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[0].ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
var result = 0m;
if (value != null)
{
decimal.TryParse(value.ToString(), out result);
}
return new object[] { result };
}
#endregion
}

I did a little digging on the Internet, and it turns out this was broken in WPF 4. Someone with an almost-identical problem to me posted here:
http://www.go4answers.com/Example/textbox-shows-old-value-being-coerced-137799.aspx
'Answer 8' states this was broken in WPF 4 and suggests a solution, which is to actually use UpdateSourceTrigger="Explicit" but to handle the TextChanged event and call BindingExpression.UpdateSource() to force changes in the text box to be reflected in the underlying value as if UpdateSourceTrigger="PropertyChanged", as per this post:
Coerce a WPF TextBox not working anymore in .NET 4.0
I implemented this, but lo and behold there were further side effects, in particular that every keystroke caused the caret to jump to the start of the text box due to updating the source and raising a PropertyChanged event. And also, any leading or trailing zeros or decimal places entered with the intention of entering further digits would get wiped out immediately. So, a simple condition to check the parsed decimal value of the text box versus the underlying value resolved this.
The following event handler is all that was needed:
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
var tb = (TextBox)e.Source;
MultiBindingExpression binding = BindingOperations.GetMultiBindingExpression(tb, TextBox.TextProperty);
decimal result = 0m;
decimal.TryParse(tb.Text, out result);
if ((decimal)GetValue(ValueProperty) != result && binding != null)
{
int caretIndex = tb.CaretIndex;
binding.UpdateSource();
tb.CaretIndex = caretIndex;
}
}

Related

Vaadin data Binder - ComboBox issues

Later Edit: I noticed that by returning one of the options in ValueProvider's apply method leads to having the check mark present, but appears to show the previous select too. I.e. if the current and previous values are distinct, two check marks are shown.
I am having troubles with ComboBox binding. I cannot get the com.vaadin.flow.data.binder.Binder properly select an option inside the combobox - i.e. tick the check mark in the dropdown.
My binder is a "generic", i.e. I am using it along with a Map, and I provide dynamic getters/setters for various map keys. So, consider Binder<Map>, while one of the properites inside the Map should be holding a Person's id.
ComboBox<Person> combobox = new ComboBox<>("Person");
List<Person> options = fetchPersons();
combobox.setItems(options);
combobox.setItemLabelGenerator(new ItemLabelGenerator<Person>() {
#Override
public String apply(final Person p) {
return p.getName();
}
});
binder.bind(combobox, new ValueProvider<Map, Person>() {
#Override
public Person apply(final Map p) {
return new Person((Long)p.get("id"), (String)p.get("name"));
}
}, new Setter<Map, Person>() {
#Override
public void accept(final Map bean, final Person p) {
bean.put("name", p.getName());
}
});
Wondering what could I possibly do wrong...
Later edit: Adding a screenshot for the Status ComboBox which has a String for caption and Integer for value.
Your problem is that you are creating a new instance in your binding, which is not working. You probably have some other bean, (I say here Bean) where Person is a property. So you want to use Binder of type Bean, to bind ComboBox to the property, which is a Person. And then populate your form with the Bean by using e.g. binder.readBean(bean). Btw. using Java 8 syntax makes your code much less verbose.
Bean bean = fetchBean();
Binder<Bean> binder = new Binder();
ComboBox<Person> combobox = new ComboBox<>("Person");
List<Person> options = fetchPersons();
combobox.setItems(options);
combobox.setItemLabelGenerator(Person::getName);
binder.forField(combobox).bind(Bean::getPerson, Bean::setPerson);
binder.readBean(bean);

Unkown Key in Vaadin 14 Grid during selection

I'm using a Grid in Vaadin 14. The grid is in multi-selection mode.
The selection handler takes a couple of seconds to complete and I'm calling setItems(...) at the end to update the items in the grid.
When the user selects another row while the previous selection handler is still running, I get an "Unknown key" error similar to the one described in https://github.com/vaadin/vaadin-grid-flow/issues/322, even though the new set of items still contains the selected item (another object instance but same according to equals()). This seems to be because the keys in the KeyMapper have already been changed due to setItems(), so the key coming from the client is not present anymore.
Is there a way to work around this, for example by disabling selection while the previous request is in progress?
UPDATE
To work around this Vaadin bug, I'm also calling setPageSize() with the exact number of items as argument. But it seems the same problem occurs even if I don't call setPageSize(), so it's probably due to setItems().
Do not change the grids items inside a SelectionListener.
You can still do all the things you wanted, but setting the items anew is not actually needed. In fact it will only create problems as you are experiencing now.
While working at this answer, I realized you will need to do your own Checkbox Column in order to be able to do actions for the one item that was just "selected", instead of removing all then add all selected ones (because much better performance). Here is how that could look.
// in my code samples, a `Foo` item can have many `Bar` items. The grid is of type Bar.
Grid.Column customSelectionColumn = grid.addComponentColumn(item -> {
Checkbox isSelected = new Checkbox();
isSelected.setValue(someParentFoo.getBars().contains(item));
isSelected.addValueChangeListener(event -> {
boolean newSelectedValue = event.getValue();
if(newSelectedValue){
someParentFoo.getBars().add(item)
} else {
someParentFoo.getBars().remove(item);
}
fooRepository.save(someParentFoo);
});
});
// make a Checkbox that selects all in the header
Checkbox toggleSelectAll = new Checkbox();
toggleSelectAll.addValueChangeListener(event -> {
if(event.getValue()){
someParentFoo.getBars().addAll(allGridItems);
} else {
someParentFoo.getBars().removeAll(allGridItems);
}
fooRepository.save(someParentFoo);
grid.getDataProvider().refreshAll(); // updates custom checkbox value of each item
});
gridHeaderRow.getCell(customSelectionColumn).setComponent(toggleSelectAll);
I solved this problem. Vaadin use data as key in HashMap. You need calc hashCode use immutable data fields. For example
public class TestData {
private int id;
private String name;
public TestData(int id) {
this.id = id;
}
#Override
public int hashCode() {
return Objects.hash(id);
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

How can I persist a check box list in MVC

I'm trying to build an html helper for creating a list of checkboxes, which will have the check state persisted using sessions. It works for the most part, remembering check box states when you check and uncheck various boxes and click submit. However, if you have boxes checked and submitted, and you go back and clear the checkboxes and resubmit (when they are ALL cleared) - it seems to want to remember the last selections. Here is what I've written...
[HomeController]
public ActionResult Index()
{
TestViewModel tvm = new TestViewModel();
return View(tvm);
}
[HttpPost]
public ActionResult Index(TestViewModel viewModel)
{
viewModel.SessionCommit();
return View(viewModel);
}
[Index View]
#model TestApp.Models.TestViewModel
#{
ViewBag.Title = "Index";
}
#using (Html.BeginForm())
{
<p>Checkboxes:</p>
#Html.CheckedListFor(x => x.SelectedItems, Model.CheckItems, Model.SelectedItems)
<input type="submit" name="Submit form" />
}
[TestViewModel]
// Simulate the checklist data source
public Dictionary<int, string> CheckItems
{
get
{
return new Dictionary<int, string>()
{
{1, "Item 1"},
{2, "Item 2"},
{3, "Item 3"},
{4, "Item 4"}
};
}
}
// Holds the checked list selections
public int[] SelectedItems { get; set; }
// Contructor
public TestViewModel()
{
SelectedItems = GetSessionIntArray("seld", new int[0] );
}
// Save selections to session
public void SessionCommit()
{
System.Web.HttpContext.Current.Session["seld"] = SelectedItems;
}
// Helper to get an int array from session
int[] GetSessionIntArray(string sessionVar, int[] defaultValue)
{
if (System.Web.HttpContext.Current.Session == null || System.Web.HttpContext.Current.Session[sessionVar] == null)
return defaultValue;
return (int[])System.Web.HttpContext.Current.Session[sessionVar];
}
[The HTML helper]
public static MvcHtmlString CheckedList(this HtmlHelper htmlHelper, string PropertyName, Dictionary<int, string> ListItems, int[] SelectedItemArray)
{
StringBuilder result = new StringBuilder();
foreach(var item in ListItems)
{
result.Append(#"<label>");
var builder = new TagBuilder("input");
builder.Attributes["type"] = "checkbox";
builder.Attributes["name"] = PropertyName;
builder.Attributes["id"] = PropertyName;
builder.Attributes["value"] = item.Key.ToString();
builder.Attributes["data-val"] = item.Key.ToString();
if (SelectedItemArray.Contains(item.Key))
builder.Attributes["checked"] = "checked";
result.Append(builder.ToString(TagRenderMode.SelfClosing));
result.AppendLine(string.Format(" {0}</label>", item.Value));
}
return MvcHtmlString.Create(result.ToString());
}
public static MvcHtmlString CheckedListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, Dictionary<int, string> ListItems, int[] SelectedItemArray)
{
var name = ExpressionHelper.GetExpressionText(expression);
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
return CheckedList(htmlHelper, name, ListItems, SelectedItemArray);
}
I've read this SO question and I think this may be to do with the model binder not knowing when there are no checkboxes checked, but even though I've gone through that and various other posts - I'm no further forward.
In one post, I saw that a hidden field is often used in combination with the checkbox to pass the 'false' state of the checkbox, but I couldn't get it working with multiple checkboxes posting back to a single property.
Can anyone shed light on this?
EDITED : to include the demonstration project I've highlighted in this post. Hopefully this will help someone to help me!
Your main issue, and the reason why the previous selections are being 'remembered' when you un-check all items is that you have a constructor in your model that calls GetSessionIntArray() which gets the values you stored last time you submitted the form. The DefaultModelBinder works by first initializing your model (including calling its default constructor) and then setting the values of its properties based the form values. In the following scenario
Step 1: Navigate to the Index() method
Assuming its the first call and no items have been added to Session, then the value of SelectedItems returned by GetSessionIntArray() is int[0], which does not match any values in CheckItems, so no checkboxes are checked.
Step 2: Check the first 2 checkboxes and submit.
The DefaultModelBinder initializes a new instance of TestViewModel and calls the constructor. The value of SelectedItems is again int[0] (nothing has been added to Session yet). The form values are then read and the value of SelectedItems is now int[1, 2] (the values of the checked checkboxes). The code inside the method is called and int[1, 2] is added to Session before returning the view.
Step 3: Un-check all checkboxes and submit again.
Your model is again initialized, but this time the constructor reads the values from Session and the value of SelectedItems is int[1,2]. The DefaultModelBinder reads the form values for SelectedItems, but there are none (un-checked checkboxes do not submit a value) so there is nothing to set and the value of SelectedItems remains int[1,2]. You then return the view and your helper checks the first 2 checkboxes based on the value of SelectedItems
You could solve this by removing the constructor from the model and modifying the code in the extension method to test for null
if (SelectedItemArray != null && SelectedItemArray.Contains(item.Key))
{
....
However there are other issues with you implementation, including
Your generating duplicate id attributes for each checkbox (your use of builder.Attributes["id"] = PropertyName;) which is invalid html.
builder.Attributes["data-val"] = item.Key.ToString(); makes no sense (it generates data-val="1", data-val="1" etc). Assuming you want attributes for unobtrusive client side validation, then the attributes would be data-val="true" data-val-required="The SelectedItems field is required.". But then you would need a associated placeholder for the error message (as generated by #Html.ValidationMessageFor() and the name attribute of each checkbox would need to be distinct (i.e. using indexers - name="[0].SelectedItems" etc).
Your using the value of the property for binding, but the correct approach (as all the built in extension method use) is to first get the value from ModelState, then from the ViewDataDictionary and finally if no values are found, then the actual model property.
You never use the value of var metadata = ModelMetadata..... although you should be (so that you can remove the last parameter (int[] SelectedItemArray) from the method, which is in effect just repeating the value of expression.
Side note: The use of a hidden field is not applicable in your case. The CheckboxFor() method generates the additional hidden input because the method binds to a bool property, and it ensures a value is always submitted.
My recommendation would be to use a package such as MvcCheckBoxList (I have not tried that one myself as I have my own extension method), at least until you spend some time studying the MVC source code to better understand how to create HtmlHelper extension methods (apologies if that sounds harsh).

JSF2 custom component (UIInput) 'value' attribute

Hi I'm trying to create a custom component that extends UIInput. In that component I generate one html input, one html submit button and one line of text. The codes are as below:
#Override
public void decode(FacesContext context) {
Map requestMap = context.getExternalContext().getRequestParameterMap();
String clientId = getClientId(context);
char sep = UINamingContainer.getSeparatorChar(context);
String symbol = ((String) requestMap.get(clientId + sep + "inputfield"));
setSubmittedValue(symbol);
}
#Override
public void encodeEnd(FacesContext context) throws IOException {
String clientId = getClientId(context);
char sep = UINamingContainer.getSeparatorChar(context);
//-----------------------this generates an html input-------------------------
encodeInputField(context, clientId + sep + "inputfield");
//Now if I uncomment the next line it generates another html, whose value always stays the same as the first one
//encodeInputField(context, clientId + sep + "inputfield2");
encodeSubmitButton(context, clientId + sep + "submit");
encodeOutputField(context);
}
private void encodeInputField(FacesContext context, String clientId) throws IOException {
// Render a standard HTML input field
ResponseWriter writer = context.getResponseWriter();
writer.startElement("input", this);
writer.writeAttribute("type", "text", null);
writer.writeAttribute("name", clientId, "clientId");
Object value = getValue();
if (value != null) {
writer.writeAttribute("value", value.toString(), "value");
}
writer.writeAttribute("size", "6", null);
writer.endElement("input");
}
private void encodeSubmitButton(FacesContext context, String clientId) throws IOException {
// render a submit button
ResponseWriter writer = context.getResponseWriter();
writer.startElement("input", this);
writer.writeAttribute("type", "Submit", null);
writer.writeAttribute("name", clientId, "clientId");
writer.writeAttribute("value", "Click Me!", null);
writer.endElement("input");
}
private void encodeOutputField(FacesContext context) throws IOException {
ResponseWriter writer = context.getResponseWriter();
//----------------weird value that comes out of nowhere-----------------------
String hellomsg = (String) getAttributes().get("value");
writer.startElement("p", this);
writer.writeText("You entered: " + hellomsg, null);
writer.endElement("p");
}
Now everything works fine but I don't understand where the value attribute in String hellomsg = (String) getAttributes().get("value"); comes. I tried to debug this program and the getAttributes() hashmap also contains weird entries and I couldn't find any entry whose key is "value".
Finally if I generate two html input then the second input's value always stays the same as the first one.
I also noticed that I can include a value in the tag, e.g. <mycc:cinput value="yes"> and when the page loads, the value of the html input generated is set to yes.
My doubt is: is every UIInput has a default value attribute? If so, is that attribute value always linked to whatever html input's value attribute? If so, is it always linked to the value attribute of the first html input generated?
Thanks in advance for reading such a long question. If possible can you guys let me know where I can find answers to issues like this? Im getting a headache browsing random google search results#_#
Thanks a lot!
Now everything works fine but I don't understand where the value attribute in String hellomsg = (String) getAttributes().get("value"); comes. I tried to debug this program and the getAttributes() hashmap also contains weird entries and I couldn't find any entry whose key is "value".
Read the javadoc of UIComponent#getAttributes(). To the point, it's really an abstract map. The get("value") doesn't really return an entry from the map, but it basically obtains the value property of the component itself by invoking its getValue() method, if any available (and it is, in case of UIInput). But if you're already sitting in the component itself, you don't need to invoke getAttributes().get("value"), you can just invoke the getValue() method directly.
Note thus that if you intend to put a custom attribute in the attribute map, then you should be using a different name, or, better, to make use of StateHelper.
See also:
JSF custom component: support for arguments of custom types, the attribute setter is never invoked
How to save state when extending UIComponentBase

Nested bean : a collection inside an object

I got a simple POJO class that i wish to display / update in a form
Using the BeanItem class and the binding of component data, i was able to quickly display the first attributes of may data class. However i've hit a wall for tow related attributes :
my class posses a set of available status, as a list of object 'AppStatus'. it also possess a current status, that is one of the status in the 'available' list.
I would like to display the list in the form as a combobox, with the current status selected.
I'we managed to associate the 'available' attribute with a combobox, but i can't seem to be able to fill this combobox when setting the data source (method setItemDataSource). How do i get the avalaible status list and the current status from my Item ?
I could always use a workaround and add a parameter to the method to get the source objet in addition to the BeanItem, but i would prefer to avoid this if the Item properties can give me my attribute.
Regards
Edit : shortened exemple, with code from Eric R.
class Status {
String id;
Sting label
+ setter /getter
}
class App {
String AppId;
String AppLabel
ArrayList<Status> availablestatus;
Status currentStatus
+setter/getter
}
in the form extension, in the createField of the fieldfactory i added the following lines
if ("status".equals(propertyId)) {
// create the combobox
ComboBox status = new ComboBox(
texts.getString("application.label.status"));
status.setItemCaptionMode(AbstractSelect.ITEM_CAPTION_MODE_PROPERTY);
status.setItemCaptionPropertyId("label");
status.setImmediate(true);
status.setNullSelectionAllowed(false);
IndexedContainer container = new IndexedContainer(
(Collection<ApplicationStatus>) item.getItemProperty(
"availableStatus").getValue());
status.setContainerDataSource(container);
status.setPropertyDataSource(item.getItemProperty("currentStatus"));
return status;
} else...
this didn't work, i do get a combobox, with the correct number of lines, but all empties.
i tried to use a beanContainer instead of a IndexedContainer
BeanContainer<String, ApplicationStatus> container =
new BeanContainer<String, ApplicationStatus>(ApplicationStatus.class);
container.addAll((Collection<ApplicationStatus>) item
.getItemProperty("availableStatus").
container.setBeanIdProperty("id");
the result is slightly better, since i do have the available values in the combobox.
only the currentValue is not selected...
I also tried to use a nestedbean property to get the id of the currentstatus, but the result is still not valid... i get a combobox, with the correct value selected, but i can not see others values anymore, since the combobox is readonly ?(even with setReadOnly(false);)
I suggest my way to resolve this. I don't think this is the nicest way, but it's works.
The beanItem class contains all you need.
I did the following in a simple project and it's work verry well :
ComboBox status = new ComboBox("ComboBox");
status.setImmediate(true);
status.setNullSelectionAllowed(false);
for(Status st : (Collection<Status>)item.getItemProperty("availableStatus").getValue()) {
status.addItem(st);
status.setItemCaption(st, st.getLabel());
}
status.setPropertyDataSource(item.getItemProperty("currentStatus"));
Hope it's works.
Regards Éric
From the vaadin demo site you can get this sample that show how to fill a combobox with countries. You could do the same i would guess (not sure I understand your problem 100%):
myForm.setFormFieldFactory(new MyFormFieldFactory ());
private class MyFormFieldFactory extends DefaultFieldFactory {
final ComboBox countries = new ComboBox("Country");
public MyFormFieldFactory () {
countries.setWidth(COMMON_FIELD_WIDTH);
countries.setContainerDataSource(ExampleUtil.getISO3166Container());
countries
.setItemCaptionPropertyId(ExampleUtil.iso3166_PROPERTY_NAME);
countries.setItemIconPropertyId(ExampleUtil.iso3166_PROPERTY_FLAG);
countries.setFilteringMode(ComboBox.FILTERINGMODE_STARTSWITH);
}
#Override
public Field createField(Item item, Object propertyId,
Component uiContext) {
Field f = (Field)item;
if ("countryCode".equals(propertyId)) {
// filtering ComboBox w/ country names
return countries;
}
return f;
}
}

Resources