I usually extend controls by creating bindable properties, in this form:
public static readonly BindableProperty OnTextProperty = BindableProperty.Create(nameof(OnText),
typeof(string), typeof(TextSwitch), defaultValue: string.Empty, defaultBindingMode: BindingMode.TwoWay,
propertyChanged: HandleOnTextPropertyChanged);
private static void HandleOnTextPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as TextSwitch)?.Rebuild();
}
public string OnText
{
get { return (string)GetValue(OnTextProperty); }
set { SetValue(OnTextProperty, value); }
}
for me, since I did some WPF, bindable properties consists of two parts: the static readonly BindableProperty field, and a corresponding property with GetValue in getter and SetValue in setter. But I stumbled over this: https://github.com/adamped/NavigationMenu/blob/master/NavigationMenu/NavigationMenu/NavigationItem.xaml.cs
which just fire the PropertyChanged event :
public static readonly BindableProperty TextProperty = BindableProperty.Create(
nameof(Text),
typeof(string),
typeof(NavigationItem),
string.Empty,
propertyChanging: (bindable, oldValue, newValue) =>
{
var ctrl = (NavigationItem)bindable;
ctrl.Text = (string)newValue;
},
defaultBindingMode: BindingMode.OneWay);
private string _text;
public string Text
{
get { return _text; }
set
{
_text = value;
OnPropertyChanged();
}
}
How can this incorporate with the bindable properties to get it working without GetValue and SetValue?, and in which cases we need to use one approach over another?
EDIT
Apparently I'm not accustomed to the notion of the self binding reusable control.. but isn't calling GetValue and SetValue essential for binding properties?
These two implementations are the same.
GetValue(BindableProperty) and SetValue are used to access the values of properties that are implemented by a BindableProperty. That is, application developers typically provide an interface for a bound property by defining public property whose get accessor casts the result of GetValue(BindableProperty) to the appropriate type and returns it, and whose set accessor uses SetValue to set the value on the correct property.
Your achievement
private static void HandleOnTextPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as TextSwitch)?.Rebuild();
}
is same with
propertyChanging: (bindable, oldValue, newValue) =>
{
var ctrl = (NavigationItem)bindable;
ctrl.Text = (string)newValue;
}
Used
OnPropertyChanged(); in setValuemethod
Related
I want to declare a BindableProperty which acts as a convenience property to another BindableProperty in Xamarin.Forms. Setting one will update the other. I won't give the full context and the actual types, but a very simple and understandable scenario would be as follows:
I have a view which defines a Number and a Numberp1 property. Number is bindable, while the Numberp1 property acts as the convenience property here.
//Definition
public class MyView : ContentView
{
public static BindableProperty NumberProperty = BindableProperty.Create(nameof(Number), typeof(int), typeof(MyView));
public int Number { get => (int)GetValue(NumberProperty); set => SetValue(NumberProperty, value); }
public int Numberp1 { get => Number + 1; set => Number = value - 1; }
}
//Usage
<local:MyView Number="{Binding Number}"/>
Things go well until a customer discovers that Numberp1 isn't bindable and would like me to make it bindable.
<local:MyView Numberp1="{Binding Numberp1}"/>
//error : No property, bindable property, or event found for 'Numberp1', or mismatching type between value and property.
How would I make both of these properties bindable but make them update each other? I tried investigating using Converters but they seem to only be usable at the Binding, not the BindableProperty definition.
Things go well until a customer discovers that Numberp1 isn't bindable and would like me to make it bindable.
Please create another BindableProperty for Numberp1,the Placeholder for the BindableProperty 'Numberp1Property ' should always match the name without 'Property'.
public static BindableProperty NumberProperty = BindableProperty.Create(nameof(Number), typeof(int), typeof(MyView11), null, propertyChanged: OnNumberChanged);
private static void OnNumberChanged(BindableObject bindable, object oldValue, object newValue)
{
var num = (MyView11)bindable;
num.Number = (int)newValue;
}
public static BindableProperty NumberpProperty = BindableProperty.Create(nameof(Numberp), typeof(int), typeof(MyView11), null, propertyChanged: OnNumber1Changed);
private static void OnNumber1Changed(BindableObject bindable, object oldValue, object newValue)
{
var num = (MyView11)bindable;
num.Numberp = (int)newValue;
}
public int Number
{
get => (int)GetValue(NumberProperty);
set => SetValue(NumberProperty, value);
}
public int Numberp
{
get => Number + 1;
set => Number = value - 1;
}
Adding propertyChanged event for BindableProperty.
They both have to be defined as BindableProperty. Unfortunately, you can't rely on the setter methods being called, because the runtime doesn't necessarily go through the setter, it can directly call SetValue, which updates the definitive source of the value for a BindableProperty.
However you can rely on the fact that BindableProperty does get you PropertyChanged notifications automatically for either of your linked properties.
So, first define both properties:
public static BindableProperty Number1Property = BindableProperty.Create(nameof(Number1), typeof(int), typeof(MyView));
public static BindableProperty Number2Property = BindableProperty.Create(nameof(Number2), typeof(int), typeof(MyView));
public int Number1 { get => (int)GetValue(Number1Property); set => SetValue(Number1Property, value); }
public int Number2 { get => (int)GetValue(Number2Property); set => SetValue(Number2Property, value); }
Then, in the constructor, listen for changes:
public MyView ()
{
InitializeComponent();
PropertyChanged += MyView_PropertyChanged;
}
And finally, make sure that changes to either one is propagated to the other, where Number2 = Number1 + 1:
private void MyView_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == Number1Property.PropertyName)
{
if (Number1 + 1 != Number2)
Number2 = Number1 + 1;
}
if (e.PropertyName == Number2Property.PropertyName)
{
if (Number1 + 1 != Number2)
Number1 = Number2 - 1;
}
}
EDIT As noted by #Knoop, the OP wanted the values to be related, not exactly the same.
I have a CustomControl with a "SetState" DependencyProperty. That is bound to a MultiBinding (one-way) with converter. That all works.
So everything is properly updated, PropertyChanged event is fired, converter gets the values, generates the output value but the SetState proeprty is not updated IF it is for example true and the converter yields to true as well. Since i want the reset logic (see code below) to execute i want SetState always to be updated even though the value it will be set to is the same as the current value.
(if the converter results in false and SetState is true SetState will be properly updated to false as expected)
MWE (typed on the fly pardon my errors)
View
<local:CustomControl>
<local:CustomControl.SetState>
<MultiBinding Converter="{local:MyConverter}">
<Binding Path="MyProp1" Mode="OneWay" UpdateSourceTrigger="PropertyChanged"/>
<Binding Path="MyProp2" Mode="OneWay" UpdateSourceTrigger="PropertyChanged"/>
</MultiBinding>
</local:CustomControl.SetState>
</local:CustomControl>
Custom Control
public class CustomControl : FrameworkElement
{
public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(
nameof(SetState),
typeof(bool),
typeof(CustomControl),
new FrameworkPropertyMetadata(false, (s, e) =>
{
((CustomControl)s).SetState = (bool)e.NewValue;
}));
private bool _state;
// For brevity i have used a boolean in the MWE
public bool SetState
{
get { return _state; }
set
{
// This is not called when the binding produces the same value as this property already has
// but i want this to be set anyway to trigger the reset logic even though SetState property already equal the value being set.
// Custom reset logic here
_state = value;
}
}
}
Converter
internal class MyConverter : MarkupExtension, IMultiValueConverter
{
private static MyConverter _instance;
public object Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
{
// Imagine some complex logic here
return ((bool)value[0] || (bool)value[1]);
}
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return _instance ?? (_instance = new MyConverter());
}
}
View model
internal class MyViewModel : INotifyPropertyChanged
{
private bool _myProp1;
private bool _myProp2;
// Imagine the properties being altered at run-time
public bool MyProp1
{
get { return _myProp1; }
private set { _myProp1 = value; OnPropertyChanged(); }
}
public bool MyProp2
{
get { return _myProp2; }
private set { _myProp2 = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The solution ended up being rather simple, for the DependencyProperty use the CoerceValueCallback instead of the PropertyChangedCallback. The former is always called, the later only if the value has actually been changed.
So in code it becomes
public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(
nameof(SetState),
typeof(bool),
typeof(CustomControl),
new PropertyMetadata(false, null,
(d, v) =>
{
((CollapsibleColumnDefinition)d).IsExpanded = (bool)v;
return v;
}));
I briefly tried to manually force the dependency property to refresh (note bug) but the CoerceValueCallback does exactly what i wanted and is cleaner.
In xaml in Xamarin.Forms, I have a custom control, I want to add property of type int. I think I have to use Bindable properties, so later I can bind a property from ViewModel.
I found this topic, but I'm not sure how to use it.. there is:
BindableProperty.Create(nameof(ItemsSource), typeof(IList), typeof(BindablePicker), null,
propertyChanged: OnItemsSourcePropertyChanged);
what's "BindablePicker"? Is it the view where property is declared?
Here's my try:
public int WedgeRating
{
get
{
return (int)GetValue(WedgeRatingProperty);
}
set
{
try
{
SetValue(WedgeRatingProperty, value);
}
catch (ArgumentException ex)
{
// We need to do something here to let the user know
// the value passed in failed databinding validation
}
}
}
public static readonly BindableProperty WedgeRatingProperty =
BindableProperty.Create(nameof(WedgeRating), typeof(int), typeof(GameCocosSharpView), null, propertyChanged: OnItemsSourcePropertyChanged);
private static void OnItemsSourcePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
}
I didn't even use it in xaml, and it already doesn't work. No particular exception. Only the page where the custom control is initialized doesn't loead. When I comment line I pasted here, it works.
Your code is good, just change your default value from null to 0 or default(int). You have it as null but an int property could never be null. This was the reason of the "crash".
public static readonly BindableProperty WedgeRatingProperty =
BindableProperty.Create (nameof (WedgeRating), typeof (int), typeof (GameCocosSharpView), default(int), propertyChanged: OnItemsSourcePropertyChanged);
Hope this helps!
Here is the example for Bindable Property
public class GameCocosSharpView : View
{
public int WedgeRating
{
get { return (int)GetValue(WedgeRatingProperty); }
set { SetValue(WedgeRatingProperty, value); }
}
public static void WedgeRatingChanged(BindableObject bindable, object oldValue, object newValue)
{
}
public static readonly BindableProperty WedgeRatingProperty =
BindableProperty.Create("WedgeRating", typeof(int), typeof(GameCocosSharpView), 1, BindingMode.Default, null, WedgeRatingChanged);
}
I want to extend the Xamarin.Forms Picker so I can bind a collection to it. After a quick search I found this page: Picker Example with two great examples. Refusing to just copy&paste the code (for learning purposes) I went on and make my own based on the two examples.
It's almost identical except mine does not work.
When I do not provide a collection to the ItemsSource everything works fine. Whenever I do assign a collection I get the following error:
Xamarin.Forms.Xaml.XamlParseException: Position 9:32. Cannot assign property "ItemsSource": type mismatch between "Xamarin.Forms.Binding" and "System.Collections.IEnumerable"
Picker:
public class BindablePicker : Picker
{
private static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create("ItemsSource", typeof(IEnumerable), typeof(BindablePicker), null, propertyChanged: OnItemsSourceChanged);
private static readonly BindableProperty SelectedItemProperty =
BindableProperty.Create("SelectedItem", typeof(object), typeof(BindablePicker));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public string DisplayMember { get; set; }
private static void OnItemsSourceChanged(BindableObject bindable, Object oldValue, Object newValue)
{
var newval = newValue as IEnumerable; //Had to implement this because of the non-generic .Create() method expects Object as param.
var picker = bindable as BindablePicker;
if (picker != null)
{
picker.Items.Clear();
if (newval == null) return;
foreach (var item in newval)
{
if (string.IsNullOrEmpty(picker.DisplayMember))
{
picker.Items.Add(item.ToString());
}
else
{
var prop = item.GetType()
.GetRuntimeProperties()
.FirstOrDefault(p => string.Equals(p.Name, picker.DisplayMember, StringComparison.OrdinalIgnoreCase));
picker.Items.Add(prop.GetValue(item).ToString());
}
}
}
}
private void OnSelectedIndexChanged(object sender, EventArgs args)
{
if (SelectedIndex < 0 || SelectedIndex > Items.Count - 1)
{
SelectedItem = null;
}
else
{
SelectedItem = ItemsSource.ItemOf(SelectedIndex); //ItemOf is an extension method I made for IEnumerable (has to be tested).
}
}
}
ViewModel (parts):
public class HomePageViewModel : ViewModelBase
{
//In the app this is populated with a List<Person>.
public IEnumerable<Person> People { get; set; }
}
XAML:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:TestApp.Controls;assembly=TestApp"
x:Class="TestApp.Views.HomePage">
<ContentPage.Content>
<StackLayout>
<controls:BindablePicker ItemsSource="{Binding People}"
HorizontalOptions="Center" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
Note that the first example picker from the linked page works with the provided VM/View setup.
I'm also not finished with the picker, I still want to provide TwoWay binding to the SelectedItem property and support for ObservableCollection.
Yours,
At first I thought I was turning mad.
Then I thought something was seriously broken.
But finally I figured it out...
private static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create("ItemsSource", typeof(IEnumerable),
typeof(BindablePicker), null, propertyChanged: OnItemsSourceChanged);
In order for the Xaml parser to see a BindableProperty as such, it has to be public (and static, but you got that part right).
In your case, the Xaml parser doesn't see the BindableProperty, so it fallback to the property, but it doesn't have any way to set the Binding, and as the types doesn't match, you get the exception.
Change your code to
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create("ItemsSource", typeof(IEnumerable),
typeof(BindablePicker), null, propertyChanged: OnItemsSourceChanged);
We have a DependencyObject which defines a Value property as a DP. It also defines a Presets collection representing friendly names for some pre-defined values.
The way our UI should work is when we bind to the Value property, if the value matches a preset, we show the friendly name, otherwise we just show the value directly.
Using a converter is out since there's no reliable way to both pass in the Presets (which are defined per-item, they aren't shared) and also do two-way binding, so our thought is to expose a FriendlyValue property on the object and use that for binding in the UI, letting it handle the conversion internally.
Since FriendlyValue depends on the already-existing Value DependencyProperty, we figured we'd just wrap the conversion logic in CLR getters/setters, but that means when the actual Value DP changes, it needs to notify the UI that FriendlyValue has been updated too, and since FriendlyValue is a CLR property, we need to support INPC for that specific property.
My question is, is that the correct/suggested way to handle this, or should I go with a second DP, monitoring its change handlers and setting the other property accordingly, adding state variables to stop one from setting the other, which then sets the first, which then again updates the other, etc.
Here's the code for the object's properties...
public static readonly DependencyProperty PresetsProperty = DependencyProperty.Register(
"Presets",
typeof(List<Preset>),
typeof(MyObject),
new UIPropertyMetadata(null));
public List<Preset> Presets
{
get { return (List<Preset>)GetValue(PresetsProperty); }
set { SetValue(PresetsProperty, value); }
}
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
"Value",
typeof(string),
typeof(MyObject),
new UIPropertyMetadata(null, (s,e) => {
var myObject = (MyObject)s;
myObject.OnPropertyChanged("FriendlyValue");
}));
public string Value
{
get { return (string)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public string FriendlyValue
{
get
{
var foundPreset = Presets.FirstOrDefault(preset => preset.Value == this.Value);
return (foundPreset != null)
? foundPreset.FriendlyName
: this.Value;
}
set
{
var foundPreset = Presets.FirstOrDefault(preset => preset.FriendlyName == value);
this.Value = (foundPreset != null)
? foundPreset.Value
: value;
// Note: We don't raise INPC notification here. It's raised in the Value's change handler
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
So is this considered good practice for built-in converter behavior?
Why not make both Value and FriendlyValue dependency properties? I see no reasons to use two techniques at the same time.
Consider:
using Preset = Tuple<string, string>;
public class MyObject : DependencyObject
{
private readonly IList<Tuple<string, string>> _presets = new List<Preset> {
new Preset("1", "good"),
new Preset("2", "bad"),
};
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
"Value", typeof(string), typeof(MyObject),
new PropertyMetadata(null,
(o, e) => ((MyObject)o).ValuePropertyChanged((string)e.NewValue)));
private static readonly DependencyProperty FriendlyValueProperty = DependencyProperty.Register(
"FriendlyValue", typeof(string), typeof(MyObject),
new PropertyMetadata(null,
(o, e) => ((MyObject)o).FriendlyValuePropertyChanged((string)e.NewValue)));
public string Value
{
get { return (string)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public string FriendlyValue
{
get { return (string)GetValue(FriendlyValueProperty); }
set { SetValue(FriendlyValueProperty, value); }
}
private void ValuePropertyChanged (string newValue)
{
var preset = _presets.FirstOrDefault(p => p.Item1 == newValue);
FriendlyValue = preset != null ? preset.Item2 : newValue;
}
private void FriendlyValuePropertyChanged (string newValue)
{
var preset = _presets.FirstOrDefault(p => p.Item2 == newValue);
Value = preset != null ? preset.Item1 : newValue;
}
}
Notes:
Code for presets simplified for brevity.
This will not cause stack overflow, because change callback are not called if value is not changed.
I hope I understood your logic correctly. There's one issue: if you change FriendlyValue to one of the friendly values from the presets, this will change Value to the value from the found preset and in turn change FriendlyValue to the name of the preset. I don't know if that behavior is expected.