One of my developers has attempted to extend ComboBox to autofilter based on what the user types:
public class AutoCompleteComboBox<T> extends ComboBox<T> {
private FilteredList<T> filteredItems;
private SortedList<T> sortedItems;
public AutoCompleteComboBox() {
setEditable(true);
setOnKeyReleased(e -> handleOnKeyReleasedEvent(e));
setOnMouseClicked(e -> handleOnMouseClicked(e));
}
private void handleOnMouseClicked(MouseEvent e) {
getItems().stream()
.filter(item -> item.toString().equals(getEditor().getText()))
.forEach(item -> getSelectionModel().select(item));
setCaretPositionToEnd();
}
private void handleOnKeyReleasedEvent(KeyEvent e) {
if (e.getCode() == KeyCode.UP || e.getCode() == KeyCode.DOWN) {
getItems().stream()
.filter(item -> item.toString().equals(getEditor().getText()))
.forEach(item -> getSelectionModel().select(item));
show();
setCaretPositionToEnd();
} else if (e.getCode() == KeyCode.ENTER || e.getCode() == KeyCode.TAB) {
String editorStr = getEditor().getText();
getSelectionModel().clearSelection();
getEditor().setText(editorStr);
setItems(this.sortedItems);
getItems().stream()
.filter(item -> item.toString().equals(editorStr))
.forEach(item -> getSelectionModel().select(item));
getEditor().selectEnd();
if (e.getCode() == KeyCode.ENTER) {
getEditor().deselect();
}
hide();
} else if (e.getText().length() == 1) {
getSelectionModel().clearSelection();
if (getEditor().getText().length() == 0) {
getEditor().setText(e.getText());
}
filterSelectionList();
show();
} else if (e.getCode() == KeyCode.BACK_SPACE && getEditor().getText().length() > 0) {
String editorStr = getEditor().getText();
getSelectionModel().clearSelection();
getEditor().setText(editorStr);
int beforeFilter = getItems().size();
filterSelectionList();
int afterFilter = getItems().size();
if (afterFilter > beforeFilter) {
hide();
}
show();
} else if (e.getCode() == KeyCode.BACK_SPACE && getEditor().getText().length() == 0) {
clearSelection();
hide();
show();
}
}
private void filterSelectionList() {
setFilteredItems();
setCaretPositionToEnd();
}
private void setFilteredItems() {
filteredItems.setPredicate(item ->
item.toString().toLowerCase().startsWith(getEditor().getText().toLowerCase()));
}
private void setCaretPositionToEnd() {
getEditor().selectEnd();
getEditor().deselect();
}
public void setInitItems(ObservableList<T> values) {
filteredItems = new FilteredList<>(values);
sortedItems = new SortedList<>(filteredItems);
setItems(this.sortedItems);
}
public void clearSelection() {
getSelectionModel().clearSelection();
getEditor().clear();
if (this.filteredItems != null) {
this.filteredItems.setPredicate(item -> true);
}
}
public T getSelectedItem() {
T selectedItem = null;
if (getSelectionModel().getSelectedIndex() > -1) {
selectedItem = getItems().get(getSelectionModel().getSelectedIndex());
}
return selectedItem;
}
public void select(String value) {
if (!value.isEmpty()) {
getItems().stream()
.filter(item -> value.equals(item.toString()))
.findFirst()
.ifPresent(item -> getSelectionModel().select(item));
}
}
}
The control works just fine, until I bind something to either the Selected or Value property. Once that happens, once I attempt to leave the field (or hit enter which causes the action to occur), I get the following exception. (Simply selecting the value with the mouse selects without issues) What I am binding to in this control is the simple object of CodeTableValue - which has two properties - both Strings, Code & Value. The toString returns the Value property.
If the control is just set up without any listeners, it works just fine. But as soon as I listen to one of the properties for the value it fails.
When I use the AutoCompleteComboBox control in my class:
#FXML private AutoCompleteComboBox<CodeTableValue> myAutoCompleteBox;
myAutoCompleteBox.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> {
System.out.println(nv);
});
The exception that is thrown:
Exception in thread "JavaFX Application Thread" java.lang.ClassCastException: java.lang.String cannot be cast to cache.CodeTableValue
at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74)
at javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
at javafx.scene.control.SelectionModel.setSelectedItem(SelectionModel.java:102)
at javafx.scene.control.ComboBox.lambda$new$152(ComboBox.java:249)
at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:105)
at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
at javafx.scene.control.ComboBoxBase.setValue(ComboBoxBase.java:150)
at com.sun.javafx.scene.control.skin.ComboBoxPopupControl.setTextFromTextFieldIntoComboBoxValue(ComboBoxPopupControl.java:405)
at com.sun.javafx.scene.control.skin.ComboBoxPopupControl.lambda$new$291(ComboBoxPopupControl.java:82)
at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyBooleanPropertyBase.fireValueChangedEvent(ReadOnlyBooleanPropertyBase.java:72)
at javafx.scene.Node$FocusedProperty.notifyListeners(Node.java:7718)
at javafx.scene.Node.setFocused(Node.java:7771)
at javafx.scene.Scene$KeyHandler.setWindowFocused(Scene.java:3932)
at javafx.scene.Scene$KeyHandler.lambda$new$11(Scene.java:3954)
at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyBooleanPropertyBase.fireValueChangedEvent(ReadOnlyBooleanPropertyBase.java:72)
at javafx.beans.property.ReadOnlyBooleanWrapper.fireValueChangedEvent(ReadOnlyBooleanWrapper.java:103)
at javafx.beans.property.BooleanPropertyBase.markInvalid(BooleanPropertyBase.java:110)
at javafx.beans.property.BooleanPropertyBase.set(BooleanPropertyBase.java:144)
at javafx.stage.Window.setFocused(Window.java:439)
at com.sun.javafx.stage.WindowPeerListener.changedFocused(WindowPeerListener.java:59)
at com.sun.javafx.tk.quantum.GlassWindowEventHandler.run(GlassWindowEventHandler.java:100)
at com.sun.javafx.tk.quantum.GlassWindowEventHandler.run(GlassWindowEventHandler.java:40)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.quantum.GlassWindowEventHandler.lambda$handleWindowEvent$423(GlassWindowEventHandler.java:150)
at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
at com.sun.javafx.tk.quantum.GlassWindowEventHandler.handleWindowEvent(GlassWindowEventHandler.java:148)
at com.sun.glass.ui.Window.handleWindowEvent(Window.java:1266)
at com.sun.glass.ui.Window.notifyFocus(Window.java:1245)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
at java.lang.Thread.run(Thread.java:745)
If the editableFlag is false, then this works. It just happens when we allow the user to type into the combo box to filter the content. Without the binding, we get no exceptions and the values are correctly set.
I have been digging deeper into this exception and it is being thrown here:
com.sun.javafx.binding.ExpressionHelper.Generic.fireValueChangedEvent()
if (curChangeSize > 0) {
final T oldValue = currentValue;
currentValue = observable.getValue();
final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue);
if (changed) {
for (int i = 0; i < curChangeSize; i++) {
try {
curChangeList[i].changed(observable, oldValue, currentValue);
} catch (Exception e) {
Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
}
}
}
}
It comes from here only when the valueProperty or selectedItemProperty is bound. Otherwise we don't get this issue.
javafx.beans.property.ObjectPropertyBase.set(T) #Override
public void set(T newValue) {
if (isBound()) {
throw new java.lang.RuntimeException((getBean() != null && getName() != null ?
getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set.");
}
if (value != newValue) {
value = newValue;
markInvalid();
}
}
I have attempted to add a StringConverter to the editor to see if that would correct the issue, but again, it continues to throw this exception. Perhaps we are attempting to handle this all wrong?
Basically we are looking to filter the combobox selection items as the user types into the field. If there is different way we should handle this please let me know, but at the moment, I think that this could be a bug in the JDK? If the field is not bound then we have no issues, but once bound, this exception occurs when the field either loses focus, or the enter key is pressed.
I've created a quick sample using your control:
#Override
public void start(Stage primaryStage) {
AutoCompleteComboBox<CodeTableValue> myAutoCompleteBox =
new AutoCompleteComboBox<>();
myAutoCompleteBox.setInitItems(FXCollections.observableArrayList(new CodeTableValue("One", "1"),
new CodeTableValue("Two", "2"), new CodeTableValue("Three", "4")));
StackPane root = new StackPane(myAutoCompleteBox);
myAutoCompleteBox.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> {
System.out.println(nv);
});
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
Based on a simple model class:
public class CodeTableValue {
private String code;
private String value;
public CodeTableValue(String code, String value) {
this.code = code;
this.value = value;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
#Override
public String toString() {
return "code=" + code + ", value=" + value;
}
}
I can reproduce your exception once I finish typing and click enter to commit the value and leave the control:
java.lang.ClassCastException: java.lang.String cannot be cast to CodeTableValue
The exception comes from the fact that the TextField used from the ComboBox in edit mode will return always a String, but you are forcing the custom ComboBox to use a CodeTableValue class.
The solution is just providing a way to convert between both String and CodeTableValue, using a StringConverter.
So I've modified your control:
public AutoCompleteComboBox(StringConverter<T> converter) {
setEditable(true);
setOnKeyReleased(e -> handleOnKeyReleasedEvent(e));
setOnMouseClicked(e -> handleOnMouseClicked(e));
super.setConverter(converter);
}
and now in the sample:
AutoCompleteComboBox<CodeTableValue> myAutoCompleteBox =
new AutoCompleteComboBox<>(new StringConverter<CodeTableValue>() {
#Override
public String toString(CodeTableValue object) {
if (object != null) {
return object.getValue();
}
return null;
}
#Override
public CodeTableValue fromString(String string) {
return new CodeTableValue(string, string);
}
});
This works now, without the ClassCastException.
Obviously you'll have to provide a way to type a string (either for code or for value) and retrieve the other out of it.
Related
I have a combobox which shows list of User objects. I have coded a custom cell factory for the combobox:
#FXML ComboBox<User> cmbUserIds;
cmbUserIds.setCellFactory(new Callback<ListView<User>,ListCell<User>>(){
#Override
public ListCell<User> call(ListView<User> l){
return new ListCell<User>(){
#Override
protected void updateItem(Useritem, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
setText(item.getId()+" "+item.getName());
}
}
} ;
}
});
ListView is showing a string(id+name), but when I select an item from listview, Combobox is showing toString() method return value i.e address of object.
I can't override toString() method, because the User domain object should be same as the one at server.
How to display id in combobox? Please suggest
EDIT1
I tried below code. Now combo box shows id when I select a value from the listview.
cmbUserIds.setConverter(new StringConverter<User>() {
#Override
public String toString(User user) {
if (user== null){
return null;
} else {
return user.getId();
}
}
#Override
public User fromString(String id) {
return null;
}
});
The selected value in combo box is cleared when control focus is lost. How to fix this?
EDIT2:
#FXML AnchorPane root;
#FXML ComboBox<UserDTO> cmbUsers;
List<UserDTO> users;
public class GateInController implements Initializable {
#Override
public void initialize(URL location, ResourceBundle resources) {
users = UserService.getListOfUsers();
cmbUsers.setItems(FXCollections.observableList(users));
cmbUsers.getSelectionModel().selectFirst();
// list of values showed in combo box drop down
cmbUsers.setCellFactory(new Callback<ListView<UserDTO>,ListCell<UserDTO>>(){
#Override
public ListCell<UserDTO> call(ListView<UserDTO> l){
return new ListCell<UserDTO>(){
#Override
protected void updateItem(UserDTO item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
setText(item.getUserId()+" "+item.getUserNm());
}
}
} ;
}
});
//selected value showed in combo box
cmbUsers.setConverter(new StringConverter<UserDTO>() {
#Override
public String toString(UserDTO user) {
if (user == null){
return null;
} else {
return user.getUserId();
}
}
#Override
public UserDTO fromString(String userId) {
return null;
}
});
}
}
Just create and set a CallBack like follows:
#FXML ComboBox<User> cmbUserIds;
Callback<ListView<User>, ListCell<User>> cellFactory = new Callback<ListView<User>, ListCell<User>>() {
#Override
public ListCell<User> call(ListView<User> l) {
return new ListCell<User>() {
#Override
protected void updateItem(User item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
setText(item.getId() + " " + item.getName());
}
}
} ;
}
}
// Just set the button cell here:
cmbUserIds.setButtonCell(cellFactory.call(null));
cmbUserIds.setCellFactory(cellFactory);
You need to provide a functional fromString() Method within the Converter!
I had the same problem as you have and as I implemented the fromString() with working code, the ComboBox behaves as expected.
This class provides a few of my objects, for dev-test purposes:
public class DevCatProvider {
public static final CategoryObject c1;
public static final CategoryObject c2;
public static final CategoryObject c3;
static {
// Init objects
}
public static CategoryObject getCatForName(final String name) {
switch (name) {
case "Kategorie 1":
return c1;
case "Cat 2":
return c2;
case "Steuer":
return c3;
default:
return c1;
}
}
}
The converter object:
public class CategoryChooserConverter<T> extends StringConverter<CategoryObject> {
#Override
public CategoryObject fromString(final String catName) {
//This is the important code!
return Dev_CatProvider.getCatForName(catName);
}
#Override
public String toString(final CategoryObject categoryObject) {
if (categoryObject == null) {
return null;
}
return categoryObject.getName();
}
}
I am trying to create a new user control as a generic lookup form opener.
lkpControl.FormToOpen="FrmProductList";
lkpControl.ReturnValueVariableName="ProductCode";
lkpControl.ShowThatForm();
if dialog result is OK:
lkpControl.txtValue=lkpControl.GetSelectedValue();
this control will open preconfigured (or specified in design time) form from its name and will return the selected item in opened form back to its textbox. Simply like getting the path from folder browser dialog.
simply a generic form opener and value getter of selected item in opened form.
I would appreciate if you could direct me to any solution or a path to follow.
Thanks in advance
UPDATE:
I've solved it by this:
public partial class LookupButton : UserControl
{
[Description("Type of the Form To Open: typeof(LookupButtonTest.Form2)")]
[Category("Custom")]
public Type FormToOpen { get; set; }
[Description("Name Of the public property to get return value from opened form.")]
[Category("Custom")]
public string PropertyToGet { get; set; }
public LookupButton()
{
InitializeComponent();
}
private void btnOpenForm_Click(object sender, EventArgs e)
{
if (FormToOpen == null)
{
throw new ArgumentNullException("FormToOpen");
}
if (PropertyToGet.Length <= 0)
{
throw new ArgumentNullException("PropertyToGet");
}
Form objForm = (Form)Activator.CreateInstance(FormToOpen);
if (objForm.ShowDialog() == DialogResult.OK)
{
bool propertyFound = false;
PropertyInfo[] props = objForm.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (PropertyInfo p in props)
{
if (p.Name == PropertyToGet)
{
txtReturnVal.Text = p.GetValue(objForm, null).ToString();
propertyFound = true;
}
}
if (!propertyFound)
{
throw new ArgumentNullException("PropertyToGet");
}
}
}
}
Something like this:
public static class ShowFormAs
{
public static T1 ShowForm<T1>(string fullTypeName, string propertyName)
where T1: class
{
return string.IsNullOrEmpty(fullTypeName)
? default(T1) :
ShowForm<T1>(System.Reflection.Assembly.GetExecutingAssembly().CreateInstance(fullTypeName) as Form, propertyName);
}
public static T1 ShowForm<T, T1>(string propertyName)
where T: Form, new ()
where T1: class
{
return ShowForm<T1>(new T(), propertyName);
}
private static T1 ShowForm<T1>(Form form, string propertyName)
where T1: class
{
if (form == null)
{
return default(T1);
}
if (form.ShowDialog() == DialogResult.OK)
{
var property = form.GetType().GetProperties().FirstOrDefault(o => string.CompareOrdinal(o.Name, propertyName) == 0);
if (property == null)
{
return default(T1);
}
return property.GetValue(form, null) as T1;
}
return default(T1);
}
}
Usage:
var x = ShowFormAs.ShowForm<Form2, string>("TTT");
var z = ShowFormAs.ShowForm<string>("StackOverflow.Form2", "TTT");
The Form2 class is:
namespace StackOverflow
{
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
}
public string TTT { get { return "Ololo"; } }
}
}
And result is:
I have built a base class for my view model(s). Here is some of the code:
public class BaseViewModel<TModel> : DependencyObject, INotifyPropertyChanged, IDisposable, IBaseViewModel<TModel>, IDataErrorInfo
{
public TModel Model { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (this._disposed)
{
return;
}
if (disposing)
{
this.Model = default(TModel);
}
this._disposed = true;
}
}
Okay, so I thought, let's add some validation to the base class, which led me to the following article: Prism IDataErrorInfo validation with DataAnnotation on ViewModel Entities. So I added the following methods / properties (IDataErrorInfo) to my base class:
string IDataErrorInfo.Error
{
get { return null; }
}
string IDataErrorInfo.this[string columnName]
{
get { return ValidateProperty(columnName); }
}
protected virtual string ValidateProperty(string columnName)
{
// get cached property accessors
var propertyGetters = GetPropertyGetterLookups(GetType());
if (propertyGetters.ContainsKey(columnName))
{
// read value of given property
var value = propertyGetters[columnName](this);
// run validation
var results = new List<ValidationResult>();
var vc = new ValidationContext(this, null, null) { MemberName = columnName };
Validator.TryValidateProperty(value, vc, results);
// transpose results
var errors = Array.ConvertAll(results.ToArray(), o => o.ErrorMessage);
return string.Join(Environment.NewLine, errors);
}
return string.Empty;
}
private static Dictionary<string, Func<object, object>> GetPropertyGetterLookups(Type objType)
{
var key = objType.FullName ?? "";
if (!PropertyLookupCache.ContainsKey(key))
{
var o = objType.GetProperties()
.Where(p => GetValidations(p).Length != 0)
.ToDictionary(p => p.Name, CreatePropertyGetter);
PropertyLookupCache[key] = o;
return o;
}
return (Dictionary<string, Func<object, object>>)PropertyLookupCache[key];
}
private static Func<object, object> CreatePropertyGetter(PropertyInfo propertyInfo)
{
var instanceParameter = System.Linq.Expressions.Expression.Parameter(typeof(object), "instance");
var expression = System.Linq.Expressions.Expression.Lambda<Func<object, object>>(
System.Linq.Expressions.Expression.ConvertChecked(
System.Linq.Expressions.Expression.MakeMemberAccess(
System.Linq.Expressions.Expression.ConvertChecked(instanceParameter, propertyInfo.DeclaringType),
propertyInfo),
typeof(object)),
instanceParameter);
var compiledExpression = expression.Compile();
return compiledExpression;
}
private static ValidationAttribute[] GetValidations(PropertyInfo property)
{
return (ValidationAttribute[])property.GetCustomAttributes(typeof(ValidationAttribute), true);
}
Okay, this brings me to the issue. The thing is the validation works perfectly, but lets say I have a property (within my view model called: Person) with a StringLength attribute. The StringLength attribute fires as soon as the application is opened. The user didn't even have a chance to do anything. The validation fires as soon as the application is started.
public class PersonViewModel : BaseViewModel<BaseProxyWrapper<PosServiceClient>>
{
private string _password = string.Empty;
[StringLength(10, MinimumLength = 3, ErrorMessage = "Password must be between 3 and 10 characters long")]
public string Password
{
get { return this._password; }
set
{
if (this._password != value)
{
this._password = value;
this.OnPropertyChanged("Password");
}
}
}
}
I have noticed that this is caused by the IDataErrorInfo.this[string columnName] property, and in turn it calls the ValidateProperty method. But, I have no idea how to fix this?
There could be two issues...
Do you populate yopur Person instance by using the public properties?
e.g.
new Person { Password = null }
This will fire the property changed notification for Password and will validate it.
Some developers also set the properties in constructors...
public class Person {
public Person() {
this.Password = null;
}
}
Recommended practise is to use private fields...
public class Person {
public Person() {
_password = null;
}
public Person(string pwd) {
_password = pwd;
}
}
OR
You can create a flag in our view model base say IsLoaded. Make sure you set it to true only after your UI is loaded (probably in UI.Loaded event). In your IDataErrorInfo.this[string columnName] check if this property is true and only then validate the values. Otherwise return null.
[EDIT]
The following change did the job:
public class PersonViewModel : BaseViewModel<BaseProxyWrapper<PosServiceClient>>
{
private string _password;
[StringLength(10, MinimumLength = 3, ErrorMessage = "Password must be between 3 and 10 characters long")]
public string Password
{
get { return this._password; }
set
{
if (this._password != value)
{
this._password = value;
this.OnPropertyChanged("Password");
}
}
}
public PersonViewModel(BaseProxyWrapper<PosServiceClient> model)
: base(model)
{
this._username = null;
}
}
Something I've done in the past is change the update source trigger to explicit, create a behavior that will update the source when the TextBox loses focus, and then attach that behavior to the TextBox.
I'm trying to databind a Button's IsMouseOver read-only dependency property to a boolean read/write property in my view model.
Basically I need the Button's IsMouseOver property value to be read to a view model's property.
<Button IsMouseOver="{Binding Path=IsMouseOverProperty, Mode=OneWayToSource}" />
I'm getting a compile error: 'IsMouseOver' property is read-only and cannot be set from markup. What am I doing wrong?
No mistake. This is a limitation of WPF - a read-only property cannot be bound OneWayToSource unless the source is also a DependencyProperty.
An alternative is an attached behavior.
As many people have mentioned, this is a bug in WPF and the best way is to do it is attached property like Tim/Kent suggested. Here is the attached property I use in my project. I intentionally do it this way for readability, unit testability, and sticking to MVVM without codebehind on the view to handle the events manually everywhere.
public interface IMouseOverListener
{
void SetIsMouseOver(bool value);
}
public static class ControlExtensions
{
public static readonly DependencyProperty MouseOverListenerProperty =
DependencyProperty.RegisterAttached("MouseOverListener", typeof (IMouseOverListener), typeof (ControlExtensions), new PropertyMetadata(OnMouseOverListenerChanged));
private static void OnMouseOverListenerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var element = ((UIElement)d);
if(e.OldValue != null)
{
element.MouseEnter -= ElementOnMouseEnter;
element.MouseLeave -= ElementOnMouseLeave;
}
if(e.NewValue != null)
{
element.MouseEnter += ElementOnMouseEnter;
element.MouseLeave += ElementOnMouseLeave;
}
}
public static void SetMouseOverListener(UIElement element, IMouseOverListener value)
{
element.SetValue(MouseOverListenerProperty, value);
}
public static IMouseOverListener GetMouseOverListener(UIElement element)
{
return (IMouseOverListener) element.GetValue(MouseOverListenerProperty);
}
private static void ElementOnMouseLeave(object sender, MouseEventArgs mouseEventArgs)
{
var element = ((UIElement)sender);
var listener = GetMouseOverListener(element);
if(listener != null)
listener.SetIsMouseOver(false);
}
private static void ElementOnMouseEnter(object sender, MouseEventArgs mouseEventArgs)
{
var element = ((UIElement)sender);
var listener = GetMouseOverListener(element);
if (listener != null)
listener.SetIsMouseOver(true);
}
}
Here's a rough draft of what i resorted to while seeking a general solution to this problem. It employs a css-style formatting to specify Dependency-Properties to be bound to model properties (models gotten from the DataContext); this also means it will work only on FrameworkElements.
I haven't thoroughly tested it, but the happy path works just fine for the few test cases i ran.
public class BindingInfo
{
internal string sourceString = null;
public DependencyProperty source { get; internal set; }
public string targetProperty { get; private set; }
public bool isResolved => source != null;
public BindingInfo(string source, string target)
{
this.sourceString = source;
this.targetProperty = target;
validate();
}
private void validate()
{
//verify that targetProperty is a valid c# property access path
if (!targetProperty.Split('.')
.All(p => Identifier.IsMatch(p)))
throw new Exception("Invalid target property - " + targetProperty);
//verify that sourceString is a [Class].[DependencyProperty] formatted string.
if (!sourceString.Split('.')
.All(p => Identifier.IsMatch(p)))
throw new Exception("Invalid source property - " + sourceString);
}
private static readonly Regex Identifier = new Regex(#"[_a-z][_\w]*$", RegexOptions.IgnoreCase);
}
[TypeConverter(typeof(BindingInfoConverter))]
public class BindingInfoGroup
{
private List<BindingInfo> _infoList = new List<BindingInfo>();
public IEnumerable<BindingInfo> InfoList
{
get { return _infoList.ToArray(); }
set
{
_infoList.Clear();
if (value != null) _infoList.AddRange(value);
}
}
}
public class BindingInfoConverter: TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string)) return true;
return base.CanConvertFrom(context, sourceType);
}
// Override CanConvertTo to return true for Complex-to-String conversions.
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string)) return true;
return base.CanConvertTo(context, destinationType);
}
// Override ConvertFrom to convert from a string to an instance of Complex.
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string text = value as string;
return new BindingInfoGroup
{
InfoList = text.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(binfo =>
{
var parts = binfo.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2) throw new Exception("invalid binding info - " + binfo);
return new BindingInfo(parts[0].Trim(), parts[1].Trim());
})
};
}
// Override ConvertTo to convert from an instance of Complex to string.
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture,
object value, Type destinationType)
{
var bgroup = value as BindingInfoGroup;
return bgroup.InfoList
.Select(bi => $"{bi.sourceString}:{bi.targetProperty};")
.Aggregate((n, p) => n += $"{p} ")
.Trim();
}
public override bool GetStandardValuesSupported(ITypeDescriptorContext context) => false;
}
public class Bindings
{
#region Fields
private static ConcurrentDictionary<DependencyProperty, PropertyChangeHandler> _Properties =
new ConcurrentDictionary<DependencyProperty, PropertyChangeHandler>();
#endregion
#region OnewayBindings
public static readonly DependencyProperty OnewayBindingsProperty =
DependencyProperty.RegisterAttached("OnewayBindings", typeof(BindingInfoGroup), typeof(Bindings), new FrameworkPropertyMetadata
{
DefaultValue = null,
PropertyChangedCallback = (x, y) =>
{
var fwe = x as FrameworkElement;
if (fwe == null) return;
//resolve the bindings
resolve(fwe);
//add change delegates
(GetOnewayBindings(fwe)?.InfoList ?? new BindingInfo[0])
.Where(bi => bi.isResolved)
.ToList()
.ForEach(bi =>
{
var descriptor = DependencyPropertyDescriptor.FromProperty(bi.source, fwe.GetType());
PropertyChangeHandler listener = null;
if (_Properties.TryGetValue(bi.source, out listener))
{
descriptor.RemoveValueChanged(fwe, listener.callback); //cus there's no way to check if it had one before...
descriptor.AddValueChanged(fwe, listener.callback);
}
});
}
});
private static void resolve(FrameworkElement element)
{
var bgroup = GetOnewayBindings(element);
bgroup.InfoList
.ToList()
.ForEach(bg =>
{
//source
var sourceParts = bg.sourceString.Split('.');
if (sourceParts.Length == 1)
{
bg.source = element.GetType()
.baseTypes() //<- flattens base types, including current type
.SelectMany(t => t.GetRuntimeFields()
.Where(p => p.IsStatic)
.Where(p => p.FieldType == typeof(DependencyProperty)))
.Select(fi => fi.GetValue(null) as DependencyProperty)
.FirstOrDefault(dp => dp.Name == sourceParts[0])
.ThrowIfNull($"Dependency Property '{sourceParts[0]}' was not found");
}
else
{
//resolve the dependency property [ClassName].[PropertyName]Property - e.g FrameworkElement.DataContextProperty
bg.source = Type.GetType(sourceParts[0])
.GetField(sourceParts[1])
.GetValue(null)
.ThrowIfNull($"Dependency Property '{bg.sourceString}' was not found") as DependencyProperty;
}
_Properties.GetOrAdd(bg.source, ddp => new PropertyChangeHandler { property = ddp }); //incase it wasnt added before.
});
}
public static BindingInfoGroup GetOnewayBindings(FrameworkElement source)
=> source.GetValue(OnewayBindingsProperty) as BindingInfoGroup;
public static void SetOnewayBindings(FrameworkElement source, string value)
=> source.SetValue(OnewayBindingsProperty, value);
#endregion
}
public class PropertyChangeHandler
{
internal DependencyProperty property { get; set; }
public void callback(object obj, EventArgs args)
{
var fwe = obj as FrameworkElement;
var target = fwe.DataContext;
if (fwe == null) return;
if (target == null) return;
var bg = Bindings.GetOnewayBindings(fwe);
if (bg == null) return;
else bg.InfoList
.Where(bi => bi.isResolved)
.Where(bi => bi.source == property)
.ToList()
.ForEach(bi =>
{
//transfer data to the object
var data = fwe.GetValue(property);
KeyValuePair<object, PropertyInfo>? pinfo = resolveProperty(target, bi.targetProperty);
if (pinfo == null) return;
else pinfo.Value.Value.SetValue(pinfo.Value.Key, data);
});
}
private KeyValuePair<object, PropertyInfo>? resolveProperty(object target, string path)
{
try
{
var parts = path.Split('.');
if (parts.Length == 1) return new KeyValuePair<object, PropertyInfo>(target, target.GetType().GetProperty(parts[0]));
else //(parts.Length>1)
return resolveProperty(target.GetType().GetProperty(parts[0]).GetValue(target),
string.Join(".", parts.Skip(1)));
}
catch (Exception e) //too lazy to care :D
{
return null;
}
}
}
And to use the XAML...
<Grid ab:Bindings.OnewayBindings="IsMouseOver:mouseOver;">...</Grid>
I would like to know how the following problem can be solved WITHOUT using Event Aggregation. This is for WPF 3.5 SP1, so the CallMethodBehavior is not available.
Simple Scenario: A click on a button inside a ControlTemplate needs to be triggered to the VM. I used CaliburnMicro's ActionMessage which worked fine. Inside the ViewModel I want to trigger a method inside the View, which only starts a custom transition (no real logic). I tried many things, but I did not work out.
I created a Property in my view, which could call the method but I am not able to use Triggers to set a new value for the property, because I can't tell the setter to target a property outside the controltemplate.
So in essence I want to update a Property in the viewmodel and trigger a set-property in the view class. Or if you have any idea how to get around this at all: I am open to new ideas! :D
Regards
Gope
i think the most simple way is to expose an event from your vm and subscribe to it in your view?
i used this for dialogs to send DialogResult from vm
I found a solution I can live with: I ported the CallMethodAction to 3.5 and wrote my own PropertyChangedTrigger. It's pretty simple to call a method inside the view via a PropertyChange in the viewmodel - Kids: don't try this at home. It's only for special scenarios! :D
Find my code below:
usage:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
<i:Interaction.Triggers >
<Framework:PropertyChangedTrigger Binding="{Binding StartTransition}" Value="True">
<Framework:CallMethodAction MethodName="ApplyTransition" />
</Framework:PropertyChangedTrigger>
</i:Interaction.Triggers>
PropertyChangedTrigger:
public class PropertyChangedTrigger : TriggerBase<DependencyObject>
{
public static readonly DependencyProperty BindingProperty = DependencyProperty.Register("Binding", typeof(object), typeof(PropertyChangedTrigger), new PropertyMetadata(new PropertyChangedCallback(OnBindingChanged)));
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(PropertyChangedTrigger), new PropertyMetadata(null));
public object Binding
{
get
{
return base.GetValue(BindingProperty);
}
set
{
base.SetValue(BindingProperty, value);
}
}
public object Value
{
get
{
return base.GetValue(ValueProperty);
}
set
{
base.SetValue(ValueProperty, value);
}
}
protected virtual void EvaluateBindingChange(object args)
{
var propertyChangedArgs = (DependencyPropertyChangedEventArgs)args;
string newValue = propertyChangedArgs.NewValue.ToString();
bool equal = string.Equals(newValue, Value.ToString(),StringComparison.InvariantCultureIgnoreCase);
if(equal)
{
InvokeActions(args);
}
}
private static void OnBindingChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
((PropertyChangedTrigger)sender).EvaluateBindingChange(args);
}
}
CallMethodAction:
public class CallMethodAction : TargetedTriggerAction<FrameworkElement>
{
private List<MethodDescriptor> methodDescriptors = new List<MethodDescriptor>();
public static readonly DependencyProperty MethodNameProperty = DependencyProperty.Register("MethodName", typeof(string), typeof(CallMethodAction), new PropertyMetadata(new PropertyChangedCallback(OnMethodNameChanged)));
public static readonly DependencyProperty TargetObjectProperty = DependencyProperty.Register("TargetObject", typeof(object), typeof(CallMethodAction), new PropertyMetadata(new PropertyChangedCallback(OnTargetObjectChanged)));
protected override void OnAttached()
{
base.OnAttached();
this.UpdateMethodInfo();
}
protected override void OnDetaching()
{
this.methodDescriptors.Clear();
base.OnDetaching();
}
private static void OnMethodNameChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
((CallMethodAction)sender).UpdateMethodInfo();
}
private static void OnTargetObjectChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
((CallMethodAction)sender).UpdateMethodInfo();
}
private static bool AreMethodParamsValid(ParameterInfo[] methodParams)
{
if (methodParams.Length == 2)
{
if (methodParams[0].ParameterType != typeof(object))
{
return false;
}
if (!typeof(EventArgs).IsAssignableFrom(methodParams[1].ParameterType))
{
return false;
}
}
else if (methodParams.Length != 0)
{
return false;
}
return true;
}
protected override void Invoke(object parameter)
{
if (base.AssociatedObject != null)
{
MethodDescriptor descriptor = this.FindBestMethod(parameter);
if (descriptor != null)
{
ParameterInfo[] parameters = descriptor.Parameters;
if (parameters.Length == 0)
{
descriptor.MethodInfo.Invoke(this.Target, null);
}
else if ((((parameters.Length == 2) && (base.AssociatedObject != null)) && ((parameter != null) && parameters[0].ParameterType.IsAssignableFrom(base.AssociatedObject.GetType()))) && parameters[1].ParameterType.IsAssignableFrom(parameter.GetType()))
{
descriptor.MethodInfo.Invoke(this.Target, new object[] { base.AssociatedObject, parameter });
}
}
else if (this.TargetObject != null)
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "No valid method found.", new object[] { this.MethodName, this.TargetObject.GetType().Name }));
}
}
}
private MethodDescriptor FindBestMethod(object parameter)
{
if (parameter != null)
{
parameter.GetType();
}
return this.methodDescriptors.FirstOrDefault(methodDescriptor => (!methodDescriptor.HasParameters || ((parameter != null) && methodDescriptor.SecondParameterType.IsAssignableFrom(parameter.GetType()))));
}
private void UpdateMethodInfo()
{
this.methodDescriptors.Clear();
if ((this.Target != null) && !string.IsNullOrEmpty(this.MethodName))
{
foreach (MethodInfo info in this.Target.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
if (this.IsMethodValid(info))
{
ParameterInfo[] parameters = info.GetParameters();
if (AreMethodParamsValid(parameters))
{
this.methodDescriptors.Add(new MethodDescriptor(info, parameters));
}
}
}
this.methodDescriptors = this.methodDescriptors.OrderByDescending<MethodDescriptor, int>(delegate(MethodDescriptor methodDescriptor)
{
int num = 0;
if (methodDescriptor.HasParameters)
{
for (Type type = methodDescriptor.SecondParameterType; type != typeof(EventArgs); type = type.BaseType)
{
num++;
}
}
return (methodDescriptor.ParameterCount + num);
}).ToList<MethodDescriptor>();
}
}
private bool IsMethodValid(MethodInfo method)
{
if (!string.Equals(method.Name, this.MethodName, StringComparison.Ordinal))
{
return false;
}
if (method.ReturnType != typeof(void))
{
return false;
}
return true;
}
public void InvokeInternal()
{
if (AssociatedObject != null)
{
foreach (
MethodInfo info in AssociatedObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
if (IsMethodValid(info))
{
info.Invoke(AssociatedObject, new object[0]);
}
}
}
}
public string MethodName
{
get
{
return (string)base.GetValue(MethodNameProperty);
}
set
{
base.SetValue(MethodNameProperty, value);
}
}
private object Target
{
get
{
return (TargetObject ?? base.AssociatedObject);
}
}
public object TargetObject
{
get
{
return base.GetValue(TargetObjectProperty);
}
set
{
base.SetValue(TargetObjectProperty, value);
}
}
private class MethodDescriptor
{
public MethodDescriptor(MethodInfo methodInfo, ParameterInfo[] methodParams)
{
MethodInfo = methodInfo;
Parameters = methodParams;
}
public bool HasParameters
{
get
{
return (Parameters.Length > 0);
}
}
public MethodInfo MethodInfo { get; private set; }
public int ParameterCount
{
get
{
return Parameters.Length;
}
}
public ParameterInfo[] Parameters { get; private set; }
public Type SecondParameterType
{
get
{
if (Parameters.Length >= 2)
{
return Parameters[1].ParameterType;
}
return null;
}
}
}
}
Hope this helps anybode. All questions are welcome! Remeber: all this can be found in the Expression Blend SDK 4. This code is only for people who are forced to work with older versions like 3.5
Regards
Gope
Gope