I am working with a custom control which has a selected items dependency property which I have wired up the collection changed event but the UI is not notified and the PropertyChanged event is always null. Normally I would say this is a datacontext issue. but I cannot change the data context on the control as no data will display.
public ObservableCollection<object> SelectedItems
{
get { return (ObservableCollection<object>)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectedItems. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.RegisterAttached("SelectedItems", typeof(ObservableCollection<object>),
typeof(MultiSelectComboBox),
new System.Windows.PropertyMetadata(new ObservableCollection<object>(), new PropertyChangedCallback(SelectedItemsPropertyChanged)));
private static void SelectedItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MultiSelectComboBox relay = d as MultiSelectComboBox;
if (e.OldValue != null)
{
var coll = (INotifyCollectionChanged)e.OldValue;
coll.CollectionChanged -= relay.SelectedItemsCollectionChanged;
}
if (e.NewValue != null)
{
var coll = (INotifyCollectionChanged)e.NewValue;
coll.CollectionChanged += relay.SelectedItemsCollectionChanged;
}
}
Above is the property declaration it is bound in xaml to an ObservableCollection on the ViewModel. What am I missing. The control implements INotifyPropertyChanged.
I have added more code below. This is the original code and I would like to change the same property to a dependency property for binding to collection puposes.
namespace Kepler.SilverlightControls.MultiSelectComboBox
{
/// <summary>
/// MultiSelect ComboBox
/// </summary>
public class MultiSelectComboBox : Telerik.Windows.Controls.RadComboBox, INotifyPropertyChanged
{
#region Events
/// <summary>
/// Est appelé quand une propriété change
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Constructor
/// <summary>
/// Initializes a new instance of MultiSelectComboBox
/// </summary>
public MultiSelectComboBox()
{
ClearSelectionButtonVisibility = Visibility.Collapsed;
string xaml = #"<DataTemplate
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:local=""clr-namespace:Kepler.SilverlightControls.MultiSelectComboBox;assembly=Kepler.SilverlightControls"">
<TextBlock TextWrapping=""Wrap"" local:MultiSelectComboBoxService.SelectionBoxLoaded=""True"" />
</DataTemplate>";
var selectionBoxTemplate = (DataTemplate)XamlReader.Load(xaml);
SelectionBoxTemplate = selectionBoxTemplate;
EmptySelectionBoxTemplate = selectionBoxTemplate;
xaml = #"<DataTemplate
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:local=""clr-namespace:Kepler.SilverlightControls.MultiSelectComboBox;assembly=Kepler.SilverlightControls"">
<CheckBox local:MultiSelectComboBoxService.ComboBoxItemLoaded=""True""
IsChecked=""{Binding Path=(local:MultiSelectComboBoxService.IsChecked), Mode=TwoWay, RelativeSource={RelativeSource Self}}"" />
</DataTemplate>";
ItemTemplate = (DataTemplate)XamlReader.Load(xaml);
}
#endregion
#region Propriétés
/// <summary>
/// IsCheckedBindingPath Property
/// </summary>
public string IsCheckedBindingPath
{
get { return (string)GetValue(IsCheckedBindingPathProperty); }
set { SetValue(IsCheckedBindingPathProperty, value); }
}
// Using a DependencyProperty as the backing store for IsCheckedBindingPath. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsCheckedBindingPathProperty =
DependencyProperty.Register("IsCheckedBindingPath", typeof(string), typeof(MultiSelectComboBox), new PropertyMetadata(null, (obj, e) =>
{
Telerik.Windows.Controls.TextSearch.SetTextPath(obj, e.NewValue as string);
}));
/// <summary>
/// DisplayBindingPath Property
/// </summary>
public static readonly DependencyProperty DisplayBindingPathProperty = DependencyProperty.Register(
"DisplayBindingPath", typeof(string), typeof(MultiSelectComboBox), new PropertyMetadata(null, (obj, e) =>
{
Telerik.Windows.Controls.TextSearch.SetTextPath(obj, e.NewValue as string);
}));
/// <summary>
/// Gets or sets the display member path (we can't reuse DisplayMemberPath property)
/// </summary>
public string DisplayBindingPath
{
get { return GetValue(DisplayBindingPathProperty) as string; }
set { SetValue(DisplayBindingPathProperty, value); }
}
private ObservableCollection<object> _selectedItems;
/// <summary>
/// Gets the selected items
/// </summary>
public ObservableCollection<object> SelectedItems
{
get
{
if (_selectedItems == null)
{
_selectedItems = new ObservableCollection<object>();
_selectedItems.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedItemsCollectionChanged);
}
return _selectedItems;
}
}
private ObservableCollection<object> _selectedValues;
/// <summary>
/// Gets the selected values
/// </summary>
public ObservableCollection<object> SelectedValues
{
get
{
if (_selectedValues == null)
{
_selectedValues = new ObservableCollection<object>();
_selectedValues.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedValuesCollectionChanged);
}
return _selectedValues;
}
}
#endregion
#region Methods
/// <summary>
/// Called when the Items property changed
/// </summary>
/// <param name="e">change informations</param>
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
int idx;
var selectedItems = SelectedItems;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Reset:
var items = e.NewItems;
if (items == null)
{
var selected = new List<object>();
foreach (var item in this.ItemsSource)
{
PropertyInfo isCheckedBindingPathProperty = this.IsCheckedBindingPath != null ? item.GetType().GetProperty(this.IsCheckedBindingPath) : null;
if (isCheckedBindingPathProperty != null
&& (bool)isCheckedBindingPathProperty.GetValue(item,null) == true)
{
selected.Add(item);
SelectedValues.Add(item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
}
}
items = selected;
}
if (items != null)
{
foreach (object value in SelectedValues)
{
foreach (object item in items)
{
if (GetSelectedValue(item).Equals(value) && !selectedItems.Contains(item))
{
selectedItems.Add(item);
}
}
}
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (object item in e.OldItems)
{
idx = selectedItems.IndexOf(item);
if (idx >= 0)
{
selectedItems.RemoveAt(idx);
}
}
break;
}
}
private void RemoveCollectionChangedEvents()
{
SelectedItems.CollectionChanged -= new NotifyCollectionChangedEventHandler(SelectedItemsCollectionChanged);
SelectedValues.CollectionChanged -= new NotifyCollectionChangedEventHandler(SelectedValuesCollectionChanged);
}
private void AddCollectionChangedEvents()
{
SelectedItems.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedItemsCollectionChanged);
SelectedValues.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedValuesCollectionChanged);
}
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (SelectedValuePath != null)
{
RemoveCollectionChangedEvents();
try
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddSelectedValues(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
RemoveSelectedValues(e.OldItems);
break;
case NotifyCollectionChangedAction.Replace:
RemoveSelectedValues(e.OldItems);
AddSelectedValues(e.NewItems);
break;
case NotifyCollectionChangedAction.Reset:
SelectedValues.Clear();
foreach (object item in Items)
{
UpdateSelectedItem(item, false);
}
AddSelectedValues(e.NewItems);
break;
}
}
finally
{
AddCollectionChangedEvents();
}
}
RaiseSelectedItemsPropertyChanged();
}
private void RemoveSelectedValues(IList items)
{
foreach (var item in items)
{
SelectedValues.Remove(GetSelectedValue(item));
UpdateSelectedItem(item, false);
}
}
private void AddSelectedValues(IList items)
{
if (items != null)
{
object selectedValue;
foreach (var item in items)
{
selectedValue = GetSelectedValue(item);
if (!SelectedValues.Contains(selectedValue))
{
SelectedValues.Add(selectedValue);
}
UpdateSelectedItem(item, true);
}
}
}
private object GetSelectedValue(object item)
{
return DataControlHelper.GetPropertyInfo(item.GetType(), SelectedValuePath).GetValue(item, null);
}
private void SelectedValuesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RemoveCollectionChangedEvents();
try
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddSelectedItems(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
RemoveSelectedItems(e.OldItems);
break;
case NotifyCollectionChangedAction.Replace:
RemoveSelectedItems(e.OldItems);
AddSelectedItems(e.NewItems);
break;
case NotifyCollectionChangedAction.Reset:
var selectedItems = SelectedItems.ToList();
SelectedItems.Clear();
foreach (object item in selectedItems)
{
UpdateSelectedItem(item, false);
}
AddSelectedItems(e.NewItems);
break;
}
}
finally
{
AddCollectionChangedEvents();
}
RaiseSelectedItemsPropertyChanged();
}
private void RaiseSelectedItemsPropertyChanged()
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItems"));
// To update the selection box
}
}
private void RemoveSelectedItems(IList values)
{
object item;
foreach (var value in values)
{
item = SelectedItems.FirstOrDefault(e => GetSelectedValue(e).Equals(value));
if (item != null)
{
SelectedItems.Remove(item);
UpdateSelectedItem(item, false);
}
}
}
private void AddSelectedItems(IList values)
{
if (values != null)
{
object item;
foreach (var value in values)
{
item = Items.FirstOrDefault(e => GetSelectedValue(e).Equals(value));
if (item != null)
{
SelectedItems.Add(item);
UpdateSelectedItem(item, true);
}
}
}
}
private void UpdateSelectedItem(object item, bool select)
{
var obj = ItemContainerGenerator.ContainerFromItem(item);
if (obj != null)
{
var cb = obj.FindChildByType<CheckBox>();
if (cb != null && cb.IsChecked != select)
{
cb.IsChecked = select;
}
}
}
/// <summary>
/// Create a new ComboBox item
/// </summary>
/// <returns>a new ComboBox item</returns>
protected override DependencyObject GetContainerForItemOverride()
{
return new MultiSelectComboBoxItem(this);
}
protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
{
base.OnKeyDown(e);
IsDropDownOpen = true;
}
#endregion
}
}
This service class is below:
namespace Kepler.SilverlightControls.MultiSelectComboBox
{
/// <summary>
/// Service for the MultiSelect comboBox
/// </summary>
public static class MultiSelectComboBoxService
{
/// <summary>
/// IsChecked property
/// </summary>
public static DependencyProperty IsCheckedProperty = DependencyProperty.RegisterAttached("IsChecked",
typeof(bool), typeof(MultiSelectComboBoxService), new PropertyMetadata(false, (obj, e) =>
{
MultiSelectComboBoxItem comboBoxItem = obj.GetVisualParent<MultiSelectComboBoxItem>();
if (comboBoxItem != null)
{
MultiSelectComboBox comboBox = comboBoxItem.ParentComboBox;
var selectedItems = (IList)comboBox.SelectedItems;
object item = comboBoxItem.DataContext;
PropertyInfo isCheckedBindingPathProperty = item.GetType().GetProperty(comboBox.IsCheckedBindingPath);
isCheckedBindingPathProperty.SetValue(item, e.NewValue,null);
if ((bool)e.NewValue)
{
if (!selectedItems.Contains(item))
{
selectedItems.Add(item);
}
}
else
{
selectedItems.Remove(item);
}
}
}));
/// <summary>
/// Gets a value indicating if the object is checked or not
/// </summary>
/// <param name="obj">DependencyObject</param>
/// <returns>a value indicating if the object is checked or not</returns>
public static bool GetIsChecked(DependencyObject obj)
{
return (bool)obj.GetValue(IsCheckedProperty);
}
/// <summary>
/// Sets a value indicating if the object is checked or not
/// </summary>
/// <param name="obj">DependencyObject</param>
/// <param name="value">the value indicating if the object is checked or not</param>
public static void SetIsChecked(DependencyObject obj, bool value)
{
obj.SetValue(IsCheckedProperty, value);
}
/// <summary>
/// SelectionBoxLoaded property called on SelectionBox load
/// </summary>
public static DependencyProperty SelectionBoxLoadedProperty = DependencyProperty.RegisterAttached("SelectionBoxLoaded",
typeof(bool), typeof(MultiSelectComboBoxService), new PropertyMetadata(false, (obj, e) =>
{
TextBlock targetElement = obj as TextBlock;
if (targetElement != null)
{
targetElement.Loaded += new RoutedEventHandler(targetElement_Loaded);
}
}));
private static void targetElement_Loaded(object sender, RoutedEventArgs e)
{
TextBlock targetElement = (TextBlock)sender;
targetElement.Loaded -= new RoutedEventHandler(targetElement_Loaded);
MultiSelectComboBox comboBox = targetElement.GetVisualParent<MultiSelectComboBox>();
if (comboBox != null)
{
targetElement.SetBinding(TextBlock.TextProperty, new Binding("SelectedItems")
{
Converter = new MultiSelectComboxConverter(),
Source = comboBox,
ConverterParameter = comboBox.DisplayBindingPath
});
}
}
/// <summary>
/// Gets the value indicating if the object is loaded or not
/// </summary>
/// <param name="obj">DependencyObject</param>
/// <returns>the value indicating if the object is loaded or not</returns>
public static bool GetSelectionBoxLoaded(DependencyObject obj)
{
return (bool)obj.GetValue(SelectionBoxLoadedProperty);
}
/// <summary>
/// Sets the value indicating if the object is loaded or not
/// </summary>
/// <param name="obj">DependencyObject</param>
/// <param name="value">the value indicating if the object is loaded or not</param>
public static void SetSelectionBoxLoaded(DependencyObject obj, bool value)
{
obj.SetValue(SelectionBoxLoadedProperty, value);
}
/// <summary>
/// ComboBoxItemLoaded called on ComboBoxItem load
/// </summary>
public static DependencyProperty ComboBoxItemLoadedProperty = DependencyProperty.RegisterAttached("ComboBoxItemLoaded",
typeof(bool), typeof(MultiSelectComboBoxService), new PropertyMetadata(false, (obj, e) =>
{
CheckBox targetElement = obj as CheckBox;
if (targetElement != null)
{
targetElement.Loaded += new RoutedEventHandler(comboBoxItem_Loaded);
targetElement.SetBinding(MultiSelectComboBoxService.DataContextProperty, new Binding());
}
}));
private static void comboBoxItem_Loaded(object sender, RoutedEventArgs e)
{
FrameworkElement element = (FrameworkElement)sender;
MultiSelectComboBox comboBox = GetComboBox(element);
if (comboBox != null)
{
element.SetBinding(CheckBox.ContentProperty, new Binding(comboBox.DisplayBindingPath));
//Binding binding = new Binding(comboBox.IsCheckedBindingPath);
//binding.Mode = BindingMode.TwoWay;
//element.SetBinding(CheckBox.IsCheckedProperty, binding);
}
}
/// <summary>
///Gets the value indicating if the item is loaded or not
/// </summary>
/// <param name="obj">DependencyObject</param>
/// <returns>the value indicating if the item is loaded or not</returns>
public static bool GetComboBoxItemLoaded(DependencyObject obj)
{
return (bool)obj.GetValue(ComboBoxItemLoadedProperty);
}
/// <summary>
/// Sets the value indicating if the item is loaded or not
/// </summary>
/// <param name="obj">DependencyObject</param>
/// <param name="value">the value indicating if the item is loaded or not</param>
public static void SetComboBoxItemLoaded(DependencyObject obj, bool value)
{
obj.SetValue(ComboBoxItemLoadedProperty, value);
}
private static MultiSelectComboBox GetComboBox(DependencyObject targetElement)
{
MultiSelectComboBoxItem item = targetElement.GetVisualParent<MultiSelectComboBoxItem>();
if (item != null)
{
return item.ParentComboBox;
}
return null;
}
private static DependencyProperty DataContextProperty = DependencyProperty.RegisterAttached("DataContext",
typeof(object), typeof(MultiSelectComboBoxService), new PropertyMetadata(null, (obj, e) =>
{
CheckBox checkBox = (CheckBox)obj;
MultiSelectComboBox comboBox = GetComboBox(checkBox);
if (comboBox != null)
{
checkBox.IsChecked = comboBox.SelectedItems.Contains(checkBox.DataContext);
}
}));
private static object GetDataContext(DependencyObject obj)
{
return obj.GetValue(DataContextProperty);
}
private static void SetDataContext(DependencyObject obj, object value)
{
obj.SetValue(DataContextProperty, value);
}
}
}
The converter is as follows:
namespace Kepler.SilverlightControls.MultiSelectComboBox
{
#region Méthodes
public class MultiSelectComboxConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string displayMemberPath = parameter as string;
if (String.IsNullOrWhiteSpace(displayMemberPath))
{
return String.Empty;
}
PropertyInfo propertyInfo;
return string.Join(", ", (value as IEnumerable<object>).Select(item =>
{
propertyInfo = DataControlHelper.GetPropertyInfo(item.GetType(), displayMemberPath);
if (propertyInfo == null)
{
return String.Empty;
}
return propertyInfo.GetValue(item, null);
}).ToArray());
}
/// <summary>
/// Not implemented
/// </summary>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}
You must not register the dependency property as an attached property, but as a regular dependency property instead. Moreover, you should not use new ObservableCollection<object>() as default property value, as that would use the same collection instance as default value for the property on all instances of your MultiSelectComboBox.
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register( // Register instead of RegisterAttached
"SelectedItems",
typeof(ObservableCollection<object>),
typeof(MultiSelectComboBox),
new PropertyMetadata(SelectedItemsPropertyChanged)); // no default value
I'd also recommend not to use ObservableCollection<object> as the property type, but simply ICollection or IEnumerable instead. This would allow for other implementations of INotifyCollectionChanged in the concrete collection type.
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register(
"SelectedItems",
typeof(ICollection),
typeof(MultiSelectComboBox),
new PropertyMetadata(SelectedItemsPropertyChanged));
public ICollection SelectedItems
{
get { return (ICollection)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
private static void SelectedItemsPropertyChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var comboBox = (MultiSelectComboBox)obj;
var oldCollection = e.OldValue as INotifyCollectionChanged;
var newCollection = e.NewValue as INotifyCollectionChanged;
if (oldCollection != null)
{
oldCollection.CollectionChanged -= SelectedItemsCollectionChanged;
}
if (newCollection != null)
{
newCollection.CollectionChanged += SelectedItemsCollectionChanged;
}
}
private static void SelectedItemsCollectionChanged(
object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
...
}
}
Please also note that the MultiSelectComboBox does not need to implement INotifyPropertyChanged. This would only be necessary for notifying changes of properties that are no dependency properties.
Related
I am following James Montemagno's tutorial to add pull-to-refresh support for my Layouts, it works perfect on Android but iOS generates the error below when I navigate to the same page as Android does.
System.InvalidCastException: < Timeout exceeded getting exception details >
The page I am trying to display is a simple StackLayout, which again works perfectly on Android.
This is my iOS renderer class from the tutorial
[assembly: ExportRenderer(typeof(RefreshableLayout), typeof(RefreshableLayoutiOS))]
namespace SocialNetwork.iOS.Renderers
{
[Preserve(AllMembers = true)]
public class RefreshableLayoutiOS : ViewRenderer<RefreshableLayout, UIView>
{
public async static void Init()
{
var temp = DateTime.Now;
}
UIRefreshControl refreshControl;
protected override void OnElementChanged(ElementChangedEventArgs<RefreshableLayout> e)
{
base.OnElementChanged(e);
if (e.OldElement != null || Element == null)
return;
refreshControl = new UIRefreshControl();
refreshControl.ValueChanged += OnRefresh;
try
{
TryInsertRefresh(this);
}
catch (Exception ex)
{
Debug.WriteLine("View is not supported in PullToRefreshLayout: " + ex);
}
UpdateColors();
UpdateIsRefreshing();
UpdateIsSwipeToRefreshEnabled();
}
bool set;
nfloat origininalY;
bool TryOffsetRefresh(UIView view, bool refreshing, int index = 0)
{
if (view is UITableView)
{
var uiTableView = view as UITableView;
if (!set)
{
origininalY = uiTableView.ContentOffset.Y;
set = true;
}
if (refreshing)
uiTableView.SetContentOffset(new CoreGraphics.CGPoint(0, origininalY - refreshControl.Frame.Size.Height), true);
else
uiTableView.SetContentOffset(new CoreGraphics.CGPoint(0, origininalY), true);
return true;
}
if (view is UICollectionView)
{
var uiCollectionView = view as UICollectionView;
if (!set)
{
origininalY = uiCollectionView.ContentOffset.Y;
set = true;
}
if (refreshing)
uiCollectionView.SetContentOffset(new CoreGraphics.CGPoint(0, origininalY - refreshControl.Frame.Size.Height), true);
else
uiCollectionView.SetContentOffset(new CoreGraphics.CGPoint(0, origininalY), true);
return true;
}
if (view is UIWebView)
{
//can't do anything
return true;
}
if (view is UIScrollView)
{
var uiScrollView = view as UIScrollView;
if (!set)
{
origininalY = uiScrollView.ContentOffset.Y;
set = true;
}
if (refreshing)
uiScrollView.SetContentOffset(new CoreGraphics.CGPoint(0, origininalY - refreshControl.Frame.Size.Height), true);
else
uiScrollView.SetContentOffset(new CoreGraphics.CGPoint(0, origininalY), true);
return true;
}
if (view.Subviews == null)
return false;
for (int i = 0; i < view.Subviews.Length; i++)
{
var control = view.Subviews[i];
if (TryOffsetRefresh(control, refreshing, i))
return true;
}
return false;
}
bool TryInsertRefresh(UIView view, int index = 0)
{
if (view is UITableView)
{
var uiTableView = view as UITableView;
uiTableView = view as UITableView;
view.InsertSubview(refreshControl, index);
return true;
}
if (view is UICollectionView)
{
var uiCollectionView = view as UICollectionView;
uiCollectionView = view as UICollectionView;
view.InsertSubview(refreshControl, index);
return true;
}
if (view is UIWebView)
{
var uiWebView = view as UIWebView;
uiWebView.ScrollView.InsertSubview(refreshControl, index);
return true;
}
if (view is UIScrollView)
{
var uiScrollView = view as UIScrollView;
view.InsertSubview(refreshControl, index);
uiScrollView.AlwaysBounceVertical = true;
return true;
}
if (view.Subviews == null)
return false;
for (int i = 0; i < view.Subviews.Length; i++)
{
var control = view.Subviews[i];
if (TryInsertRefresh(control, i))
return true;
}
return false;
}
BindableProperty rendererProperty;
BindableProperty RendererProperty
{
get
{
if (rendererProperty != null)
return rendererProperty;
var type = Type.GetType("Xamarin.Forms.Platform.iOS.Platform, Xamarin.Forms.Platform.iOS");
var prop = type.GetField("RendererProperty");
var val = prop.GetValue(null);
rendererProperty = val as BindableProperty;
return rendererProperty;
}
}
void UpdateColors()
{
if (RefreshView == null)
return;
if (RefreshView.RefreshColor != Color.Default)
refreshControl.TintColor = RefreshView.RefreshColor.ToUIColor();
if (RefreshView.RefreshBackgroundColor != Color.Default)
refreshControl.BackgroundColor = RefreshView.RefreshBackgroundColor.ToUIColor();
}
void UpdateIsRefreshing()
{
IsRefreshing = RefreshView.IsRefreshing;
}
void UpdateIsSwipeToRefreshEnabled()
{
refreshControl.Enabled = RefreshView.IsPullToRefreshEnabled;
}
public RefreshableLayout RefreshView
{
get { return Element; }
}
bool isRefreshing;
public bool IsRefreshing
{
get { return isRefreshing; }
set
{
bool changed = IsRefreshing != value;
isRefreshing = value;
if (isRefreshing)
refreshControl.BeginRefreshing();
else
refreshControl.EndRefreshing();
if (changed)
TryOffsetRefresh(this, IsRefreshing);
}
}
void OnRefresh(object sender, EventArgs e)
{
if (RefreshView?.RefreshCommand?.CanExecute(RefreshView?.RefreshCommandParameter) ?? false)
{
RefreshView.RefreshCommand.Execute(RefreshView?.RefreshCommandParameter);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == RefreshableLayout.IsPullToRefreshEnabledProperty.PropertyName)
UpdateIsSwipeToRefreshEnabled();
else if (e.PropertyName == RefreshableLayout.IsRefreshingProperty.PropertyName)
UpdateIsRefreshing();
else if (e.PropertyName == RefreshableLayout.RefreshColorProperty.PropertyName)
UpdateColors();
else if (e.PropertyName == RefreshableLayout.RefreshBackgroundColorProperty.PropertyName)
UpdateColors();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (refreshControl != null)
{
refreshControl.ValueChanged -= OnRefresh;
}
}
}
}
I got the code this tutorial and this GitHub
Edit:
XAML
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="SocialNetwork.TestScrollPage" xmlns:local="clr-namespace:SocialNetwork.Renderers">
<ContentPage.Content>
<StackLayout>
<local:RefreshableLayout x:Name="RefreshableLayout" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<StackLayout>
</StackLayout>
</local:RefreshableLayout>
</StackLayout>
</ContentPage.Content>
CS:
public partial class TestScrollPage : ContentPage
{
public TestScrollPage ()
{
InitializeComponent ();
RefreshableLayout.RefreshCommand = new Command(() => RefreshPage());
}
public void RefreshPage()
{
RefreshableLayout.IsRefreshing = false;
DisplayAlert("ok", "ok", "ok");
}
}
And I navigate to the page using Detail = new TestScrollPage();
Edit 2:
public partial class RefreshableLayout : ContentView
{
public static readonly BindableProperty IsRefreshingProperty =
BindableProperty.Create(nameof(IsRefreshing), typeof(bool), typeof(RefreshableLayout), false);
/// <summary>
/// Gets or sets a value indicating whether this instance is refreshing.
/// </summary>
/// <value><c>true</c> if this instance is refreshing; otherwise, <c>false</c>.</value>
public bool IsRefreshing
{
get { return (bool)GetValue(IsRefreshingProperty); }
set
{
if ((bool)GetValue(IsRefreshingProperty) == value)
OnPropertyChanged(nameof(IsRefreshing));
SetValue(IsRefreshingProperty, value);
}
}
/// <summary>
/// The is pull to refresh enabled property.
/// </summary>
public static readonly BindableProperty IsPullToRefreshEnabledProperty =
BindableProperty.Create(nameof(IsPullToRefreshEnabled), typeof(bool), typeof(RefreshableLayout), true);
/// <summary>
/// Gets or sets a value indicating whether this instance is pull to refresh enabled.
/// </summary>
/// <value><c>true</c> if this instance is pull to refresh enabled; otherwise, <c>false</c>.</value>
public bool IsPullToRefreshEnabled
{
get { return (bool)GetValue(IsPullToRefreshEnabledProperty); }
set { SetValue(IsPullToRefreshEnabledProperty, value); }
}
/// <summary>
/// The refresh command property.
/// </summary>
public static readonly BindableProperty RefreshCommandProperty =
BindableProperty.Create(nameof(RefreshCommand), typeof(ICommand), typeof(RefreshableLayout));
/// <summary>
/// Gets or sets the refresh command.
/// </summary>
/// <value>The refresh command.</value>
public ICommand RefreshCommand
{
get { return (ICommand)GetValue(RefreshCommandProperty); }
set { SetValue(RefreshCommandProperty, value); }
}
/// <summary>
/// Gets the Refresh command
/// </summary>
public static readonly BindableProperty RefreshCommandParameterProperty =
BindableProperty.Create(nameof(RefreshCommandParameter),
typeof(object),
typeof(RefreshableLayout),
null,
propertyChanged: (bindable, oldvalue, newvalue) => ((RefreshableLayout)bindable).RefreshCommandCanExecuteChanged(bindable, EventArgs.Empty));
/// <summary>
/// Gets or sets the Refresh command parameter
/// </summary>
public object RefreshCommandParameter
{
get { return GetValue(RefreshCommandParameterProperty); }
set { SetValue(RefreshCommandParameterProperty, value); }
}
/// <summary>
/// Executes if enabled or not based on can execute changed
/// </summary>
/// <param name="sender"></param>
/// <param name="eventArgs"></param>
void RefreshCommandCanExecuteChanged(object sender, EventArgs eventArgs)
{
ICommand cmd = RefreshCommand;
if (cmd != null)
IsEnabled = cmd.CanExecute(RefreshCommandParameter);
}
/// <summary>
/// Color property of refresh spinner color
/// </summary>
public static readonly BindableProperty RefreshColorProperty =
BindableProperty.Create(nameof(RefreshColor), typeof(Color), typeof(RefreshableLayout), Color.Default);
/// <summary>
/// Refresh color
/// </summary>
public Color RefreshColor
{
get { return (Color)GetValue(RefreshColorProperty); }
set { SetValue(RefreshColorProperty, value); }
}
/// <summary>
/// Color property of refresh background color
/// </summary>
public static readonly BindableProperty RefreshBackgroundColorProperty =
BindableProperty.Create(nameof(RefreshBackgroundColor), typeof(Color), typeof(RefreshableLayout), Color.Default);
/// <summary>
/// Refresh background color
/// </summary>
public Color RefreshBackgroundColor
{
get { return (Color)GetValue(RefreshBackgroundColorProperty); }
set { SetValue(RefreshBackgroundColorProperty, value); }
}
/// <param name="widthConstraint">The available width for the element to use.</param>
/// <param name="heightConstraint">The available height for the element to use.</param>
/// <summary>
/// Optimization as we can get the size here of our content all in DIP
/// </summary>
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
if (Content == null)
return new SizeRequest(new Size(100, 100));
return base.OnMeasure(widthConstraint, heightConstraint);
}
}
Please read this documentation about Xamarin Liver Player. It declares the limitations:
Custom Renderers are not supported for Xamarin Forms.
Also there are some other limitions or issues when you use Xamarin Liver Player. So I recommend you to use simulators or a real physical device to test your project.
If you don't have a Mac. You can also try to download an Enterprise Visual Studio to let the simulators mapping to Windows.
We have our own TabControl class which does an override on OnSelectionChanged.
The relevant code is:
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
UpdateSelectedItem();
}
internal Grid ItemsHolder { get; set; }
public TabControl()
: base()
{
ItemsHolder = new Grid();
// this is necessary so that we get the initial databound selected item
this.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
/// <summary>
/// if containers are done, generate the selected item
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
UpdateSelectedItem();
}
}
private bool _isTemplateApplied = false;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var itemsHolderParent = GetTemplateChild("PART_ItemsHolderParent") as Panel;
if (itemsHolderParent != null)
{
var parent = ItemsHolder.Parent as Panel;
if (parent != null)
parent.Children.Remove(ItemsHolder);
itemsHolderParent.Children.Add(ItemsHolder);
if (parent != null)
ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
UpdateSelectedItem();
}
_isTemplateApplied = true;
}
internal void ClearChildren()
{
foreach (var cp in ItemsHolder.Children.OfType<ContentPresenter>())
cp.Content = null;
ItemsHolder.Children.Clear();
}
/// <summary>
/// when the items change we remove any generated panel children and add any new ones as necessary
/// </summary>
/// <param name="e"></param>
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
switch (e.Action)
{
case NotifyCollectionChangedAction.Reset:
var removeList = new List<ContentPresenter>();
foreach (var presenter in ItemsHolder.Children.OfType<ContentPresenter>())
removeList.Add(presenter);
var oldItemsCount = removeList.Count;
foreach (var item in Items)
{
var itemPresenter = FindChildContentPresenter(item);
if (removeList.Contains(itemPresenter))
removeList.Remove(itemPresenter);
}
foreach (var removePresenter in removeList)
ItemsHolder.Children.Remove(removePresenter);
//If there were old items, the SelectionChanged in the Selector will force a new selected item
//If there are no items we can't update the selected item (there is nothing to select)
//If the tempalte is not yet applied, applying the template will select the tabitem
if (oldItemsCount == 0 && Items != null && Items.Count > 0 && _isTemplateApplied)
UpdateSelectedItem();
break;
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
ContentPresenter cp = FindChildContentPresenter(item);
if (cp != null)
{
ItemsHolder.Children.Remove(cp);
}
}
}
// don't do anything with new items because we don't want to
// create visuals that aren't being shown
UpdateSelectedItem();
break;
case NotifyCollectionChangedAction.Replace:
throw new NotImplementedException("Replace not implemented yet");
}
}
/// <summary>
/// generate a ContentPresenter for the selected item
/// </summary>
internal void UpdateSelectedItem()
{
if (SelectedIndex == -1 && Items != null && Items.Count > 0)
SelectedIndex = 0;
if (SelectedIndex == -1)
return;
// generate a ContentPresenter if necessary
TabItem item = GetSelectedTabItem();
if (item != null)
{
FindOrElseCreateChildContentPresenter(item);
}
// show the right child
foreach (ContentPresenter child in ItemsHolder.Children.OfType<ContentPresenter>())
{
if ((child.Tag as TabItem).IsSelected)
SelectedTabItem = child.Tag as TabItem;
child.Visibility = ((child.Tag as TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed;
}
}
/// <summary>
/// create the child ContentPresenter for the given item (could be data or a TabItem)
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
internal ContentPresenter FindOrElseCreateChildContentPresenter(object item)
{
if (item == null)
{
return null;
}
ContentPresenter cp = FindChildContentPresenter(item);
if (cp != null)
{
cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
return cp;
}
// the actual child to be added. cp.Tag is a reference to the TabItem
cp = new ContentPresenter();
cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
Dispatcher.BeginInvoke(new Action(() =>
{
cp.ContentTemplate = this.SelectedContentTemplate;
cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
cp.ContentStringFormat = this.SelectedContentStringFormat;
}), System.Windows.Threading.DispatcherPriority.Send);
cp.Visibility = Visibility.Collapsed;
cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
ItemsHolder.Children.Add(cp);
return cp;
}
/// <summary>
/// Find the CP for the given object. data could be a TabItem or a piece of data
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public ContentPresenter FindChildContentPresenter(object data)
{
if (data is TabItem)
{
data = (data as TabItem).Content;
}
if (data == null)
{
return null;
}
foreach (ContentPresenter cp in ItemsHolder.Children.OfType<ContentPresenter>())
{
if (cp.Content == data)
{
return cp;
}
}
return null;
}
/// <summary>
/// copied from TabControl; wish it were protected in that class instead of private
/// </summary>
/// <returns></returns>
protected TabItem GetSelectedTabItem()
{
object selectedItem = base.SelectedItem;
if (selectedItem == null)
{
return null;
}
TabItem item = selectedItem as TabItem;
if (item == null)
{
item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
}
return item;
}
internal TabItem SelectedTabItem
{
get { return (TabItem)GetValue(SelectedTabItemProperty); }
set { SetValue(SelectedTabItemProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectedTabItem. This enables animation, styling, binding, etc...
internal static readonly DependencyProperty SelectedTabItemProperty =
DependencyProperty.Register("SelectedTabItem", typeof(TabItem), typeof(TabControl), new UIPropertyMetadata(null));
When I use the debugger I see that sometimes the OnSelectionChanged is fired multiple times for one tab switch. Is this a bug? How can I fix this? Or is it intended behaviour and can I use another event to detect tab switches?
If you set selectedValue of the TabControl in your UpdateSelectedItem method, It is going to enter the code block more than once.For instance, if you set
private void UpdateSelectedItem()
{
this.SelectedValue = 0; // set to a value
}
like this code block you will see the debugger enters OnSelectionChanged method twice.
I'm working with VS 2015 on a dynamic WPF-application, where the controls and their binding are generated in code-behind.
Now i wanted to implement a TextBox which is bound to a datatable's column of type double.
Further in my builder class for that TextBox i tried to set the Format and the MaxLength.
This all works fine.
The Format and the MaxLength are okay, but when i want to input or edit something, it's very difficult to do.
The customers will never accept that.
What can i do?
Here i call the builder class:
CreateTextbox(viewmodel, "table1", "Menge", 20, 40, 60, "#0.0000", 2, 4);
And this is the public method which creates and returns the TextBox.
/// <summary>
/// Create and returns a textbox.
/// </summary>
/// <param name="viewmodel">The viewmodel.</param>
/// <param name="tablename">The name of the datatable.</param>
/// <param name="sourceColumn">The column to which we want to bind the textbox.</param>
/// <param name="leftPos">The left position.</param>
/// <param name="topPos">The top position.</param>
/// <param name="width">The width of the textbox.</param>
/// <param name="format">The format of the value.</param>
/// <param name="integerPositions">The integerpositions before the comma.</param>
/// <param name="decimalPlaces">The decimal places after the comma.</param>
/// <returns>Returns an object of type TextBox.</returns>
public TextBox CreateTextbox(MainViewModel viewmodel, string tablename, string sourceColumn, double leftPos, double topPos,
double width, string format="", int integerPositions=0, int decimalPlaces=0)
{
// Textbox is created.
TextBox textbox = new TextBox();
textbox.Width = width;
Canvas.SetLeft(textbox, leftPos);
Canvas.SetTop(textbox, topPos);
// Now the binding to the datasource is initialized.
Binding controlbinding = new Binding();
controlbinding.Source = viewmodel.ApplicationDataSet.Tables[tablename].DefaultView;
controlbinding.Path = new PropertyPath("[0][" + sourceColumn + "]");
if (!string.IsNullOrWhiteSpace(format))
{
controlbinding.StringFormat = format;
textbox.MaxLength = integerPositions + decimalPlaces + 1;
////controlbinding.ValidationRules = ???;
}
controlbinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
textbox.SetBinding(TextBox.TextProperty, controlbinding);
return textbox;
}
yyyy
You could try to create e.g. a behavior that could look similar to that:
public class TextBoxNumericalOnlyBehavior : Behavior<Control>
{
private TextBox textBox;
public static readonly DependencyProperty MaxValueProperty =
DependencyProperty.Register("MaxValue", typeof(Double), typeof(TextBoxNumericalOnlyBehavior),
new FrameworkPropertyMetadata(Double.MaxValue));
public Double MaxValue
{
get
{
return (Double)base.GetValue(MaxValueProperty);
}
set
{
base.SetValue(MaxValueProperty, value);
}
}
public static readonly DependencyProperty MinValueProperty =
DependencyProperty.Register("MinValue", typeof(Double), typeof(TextBoxNumericalOnlyBehavior),
new FrameworkPropertyMetadata(Double.MinValue));
public Double MinValue
{
get
{
return (Double)base.GetValue(MinValueProperty);
}
set
{
base.SetValue(MinValueProperty, value);
}
}
public static readonly DependencyProperty RegularExpressionProperty =
DependencyProperty.Register("RegularExpression", typeof(string), typeof(TextBoxNumericalOnlyBehavior),
new FrameworkPropertyMetadata(".*"));
public string RegularExpression
{
get
{
return (string)base.GetValue(RegularExpressionProperty);
}
set
{
base.SetValue(RegularExpressionProperty, value);
}
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += AssociatedObject_Loaded;
AssociatedObject.Unloaded += AssociatedObject_Unloaded;
AssociatedObject.PreviewKeyDown += AssociatedObject_PreviewKeyDown;
AssociatedObject.PreviewTextInput += AssociatedObject_PreviewTextInput;
DataObject.AddPastingHandler(AssociatedObject, OnPaste);
}
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
// to do
}
private void AssociatedObject_Unloaded(object sender, RoutedEventArgs e)
{
// to do
}
private void AssociatedObject_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (this.textBox != null)
{
if (e.Key == Key.Space || e.Key == Key.Delete)
{
e.Handled = true;
}
else if (e.Key == Key.Back)
{
e.Handled = !IsValid(string.Empty, false, true);
}
}
}
private void AssociatedObject_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (this.textBox != null)
{
e.Handled = !IsValid(e.Text, false, false);
}
}
private void OnPaste(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(DataFormats.Text))
{
string text = Convert.ToString(e.DataObject.GetData(DataFormats.Text));
if (!IsValid(text, true, false))
{
e.CancelCommand();
}
}
else
{
e.CancelCommand();
}
}
private bool IsValid(string text, bool paste, bool back)
{
if(this.textBox.Text == null)
{
return false;
}
int caretIndex = this.textBox.CaretIndex;
int selectionStart = this.textBox.SelectionStart;
int selectionLength = this.textBox.SelectionLength;
string newValue = string.Empty;
// on paste
if (paste && selectionLength > 0)
{
string tempValue = this.textBox.Text.Remove(selectionStart, selectionLength);
newValue = tempValue.Insert(caretIndex, text.Trim());
}
// on back
else if (back)
{
// check if textbox is empty
if (this.textBox.Text.Equals(string.Empty))
{
return true;
}
newValue = this.textBox.Text.Remove(selectionStart == 0 ? selectionStart : selectionStart - 1,
selectionLength == 0 ? 1 : selectionLength);
}
else
{
newValue = this.textBox.Text.Insert(caretIndex, text.Trim());
}
return IsInRange(newValue);
}
private bool IsInRange(string text)
{
Double result = 0;
if (text.Equals(string.Empty))
{
return true;
}
if (Regex.IsMatch(text, this.RegularExpression))
{
if (Double.TryParse(text, out result))
{
return result <= this.MaxValue && result >= this.MinValue;
}
}
return false;
}
}
and .xaml:
<i:Interaction.Behaviors>
<commonBehaviors:TextBoxNumericalOnlyBehavior MaxValue="99.99"
MinValue="0.00"
RegularExpression="^(\d{0,2})(\.\d{0,2})?$"/>
</i:Interaction.Behaviors>
Regular exp: RegularExpression="^(\d{0,2})(\.\d{0,2})?$"/> will allow only 00.00 format, but let's say RegularExpression="^(\d{0,4})(\.\d{0,2})?$"/> will allow 0000.00. Max and Min Values set a range of input.
I want to create a editable Spinner (or Picker in Xamarin.Forms).
I have a custom renderer for my element (derived from Picker) that render the Picker as AutoCompleteTextView. Inside the renderer i have created AutoCompleteTextView that shows the dropdown menue if it on focus or is been clicked. Its worked fine.
My problem is that it shows like a EditText (or Entry in Xamarin.Forms) control on device, but i want to display it like a Spinner (or Picker on Xamarin.Forms).
Any idea how i have to make this?
EDIT:
Here what i do in UWP:
Custom Renderer for UWP control:
CustomEditablePicker customControl; // Derived from Xamarin.Forms.Picker
ComboBox nativeControl; // Windows.UI.Xaml.Controls.ComboBox
TextBox editControl;
protected override void OnElementChanged(ElementChangedEventArgs<CustomEditablePicker> e)
{
base.OnElementChanged(e);
customControl = e.NewElement;
nativeControl = new ComboBox();
editControl = new TextBox(); // First element of CheckBox would be a TextBox for edit some Text
// Set the style (declarated in App.xaml)
Style editableStyle = App.Current.Resources["ComboBoxItemTextBox"] as Style;
if (editableStyle != null)
{
editControl.Style = editableStyle;
ComboBoxItem item = new ComboBoxItem();
item.IsSelected = true; // Select First element
item.Content = editControl; // TextBox as content for first element
nativeControl.Items.Add(item);
nativeControl.SelectionChanged += NativeControl_SelectionChanged; // Do something if selection is changed
}
// Add items from custom element to native element
foreach (var item in customControl.Items)
{
nativeControl.Items.Add(item);
}
editControl.KeyDown += EditControl_KeyDown; // Handle the space key
editControl.TextChanged += EditControl_TextChanged; // Handle something if text inside TextBox is changed
base.SetNativeControl(nativeControl); // Set native control to be displayed
}
/// <summary>
/// Set text for Picker if value is changed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void EditControl_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox edit = (sender as TextBox);
customControl.Text = edit.Text;
}
/// <summary>
/// Handle Space-Key, without handle this key the ComboBox would be lost focus
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void EditControl_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == Windows.System.VirtualKey.Space)
{
if (editControl.SelectionLength > 0)
{
editControl.Text = editControl.Text.Remove(editControl.SelectionStart, editControl.SelectionLength);
editControl.SelectionLength = 0;
}
int pos = editControl.SelectionStart;
editControl.Text = editControl.Text.Insert(pos, " ");
editControl.SelectionStart = pos + 1;
e.Handled = true;
}
}
/// <summary>
/// Occurs when selection of the box is changed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void NativeControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 1 && e.AddedItems[0] != (sender as ComboBox).Items[0])
{
(sender as ComboBox).SelectedIndex = 0;
editControl.Text = e.AddedItems[0] as String;
}
}
And The control in the PCL (Xamarin.Forms):
public class CustomEditablePicker : Picker
{
public static readonly BindableProperty EditTextProperty = BindableProperty.Create<CustomEditablePicker, string>(c => c.Text, String.Empty, BindingMode.TwoWay, propertyChanged: OnTextChanged);
public event EventHandler<CustomUIEventArgs<string>> TextChanged;
public static readonly BindableProperty Source = BindableProperty.Create<CustomEditablePicker, IEnumerable<string>>(l => l.ItemsSource, new List<string>(), BindingMode.TwoWay, propertyChanged: OnSourceChanged);
private static void OnSourceChanged(BindableObject bindable, IEnumerable<string> oldValue, IEnumerable<string> newValue)
{
CustomEditablePicker customEditablePicker = (CustomEditablePicker)bindable;
customEditablePicker.ItemsSource = newValue;
}
public event EventHandler<CustomUIEnumerableArgs<IEnumerable<string>>> SourceChanged;
public IEnumerable<string> ItemsSource
{
get { return (List<string>)this.GetValue(Source); }
set
{
if (this.ItemsSource != value)
{
this.SetValue(Source, value);
if (SourceChanged != null)
{
this.SourceChanged.Invoke(this, new CustomUIEnumerableArgs<IEnumerable<string>>(value));
}
}
}
}
public string Text
{
get { return (string)this.GetValue(EditTextProperty); }
set
{
if (this.Text != value)
{
this.SetValue(EditTextProperty, value);
if (TextChanged != null)
{
// Raise a event, with changed text
this.TextChanged.Invoke(this, new CustomUIEventArgs<string>(value));
}
}
}
}
private static void OnTextChanged(BindableObject bindable, string oldValue, string newValue)
{
CustomEditablePicker customEditablePicker = (CustomEditablePicker)bindable;
customEditablePicker.Text = newValue;
}
}
To show image inside EditText, use SetCompoundDrawablesWithIntrinsicBounds:
protected override void OnElementChanged(ElementChangedEventArgs<SoundsPicker> e)
{
if (e.NewElement != null)
{
if (base.Control == null)
{
EditText editText = new EditText(Context)
{
Focusable = false,
Clickable = true,
Tag = this
};
var padding = (int)Context.ToPixels(10);
// that show image on right side
editText.SetCompoundDrawablesWithIntrinsicBounds(0, 0, Resource.Drawable.arrow_down, 0);
editText.CompoundDrawablePadding = padding;
editText.SetOnClickListener(MyPickerPickerListener.Instance);
editText.SetBackgroundDrawable(null);
SetNativeControl(editText);
}
}
base.OnElementChanged(e);
}
Where is Resource.Drawable.arrow_down is your arrow image.
You can use tools like ILSpy or dotPeek to look at code inside Xamarin assembly.
I have a quickFind TextBox. I want to filter a collection for records that contain the quickFind string.
How can I delay the search until the user has stopped typing for 2 seconds?
Here's the ReactiveUI way to do the whole thing (filtering the items after a 2sec delay):
// These are defined in your ViewModel class as settable Properties
string FilterText;
ReactiveList<Record> ListOfRecords;
IReactiveDerivedList<Record> FilteredRecords;
// This is in your ViewModel constructor
FilteredRecords = ListOfRecords.CreateDerivedCollection(
x => !String.IsNullOrWhiteSpace(FilterText) ? recordContainsString(FilterText) : true,
x => x.Id,
this.WhenAnyValue(x => x.FilterText).Throttle(TimeSpan.FromSeconds(2.0));
If all you want to do is find out when a property has changed but after an idle time, it's:
this.WhenAnyValue(x => x.SomeProperty)
.Throttle(TimeSpan.FromSeconds(2.0), RxApp.MainThreadScheduler)
.Subscribe(x => Console.WriteLine("The item is " + x);
I love the Timer and Lock example because it shows how much easier ReactiveUI is :)
below is a class that I'm hoping does the trick for you. including sample usage shown at the bottom.
public class EventDelayer
{
/// <summary>
/// Contains info on an individual event that was queued;
/// </summary>
public class DelayedEventInfo
{
private readonly object _sender;
private readonly EventArgs _eventArgs;
private readonly DateTime _eventTime;
public DelayedEventInfo(object sender, EventArgs eventArgs, DateTime eventTime)
{
_sender = sender;
_eventArgs = eventArgs;
_eventTime = eventTime;
}
public object Sender { get { return _sender; } }
public EventArgs EventArgs { get { return _eventArgs; } }
public DateTime EventTime { get { return _eventTime; } }
}
/// <summary>
/// contains a list of
/// </summary>
public class DelayedEventArgs : EventArgs, IEnumerable<DelayedEventInfo>
{
private readonly List<DelayedEventInfo> _eventInfos;
public DelayedEventArgs(IEnumerable<DelayedEventInfo> eventInfos)
{
_eventInfos = new List<DelayedEventInfo>(eventInfos);
}
public IEnumerator<DelayedEventInfo> GetEnumerator()
{
return _eventInfos.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _eventInfos.GetEnumerator();
}
}
private readonly List<DelayedEventInfo> _infoList = new List<DelayedEventInfo>();
private readonly TimeSpan _delayTime;
private readonly object _lock = new object();
private System.Threading.Timer _timer;
public event EventHandler<DelayedEventArgs> DelayedEvent;
public EventDelayer(TimeSpan delayTime)
{
_delayTime = delayTime;
}
/// <summary>
/// call to 'enqueue' an event.
/// </summary>
public void Enqueue(object sender, EventArgs args)
{
lock (_lock)
{
_infoList.Add(new DelayedEventInfo(sender, args, DateTime.Now));
if (_timer != null)
{
_timer.Dispose();
_timer = null;
}
_timer = new System.Threading.Timer(ThreadProc, this, _delayTime, TimeSpan.FromMilliseconds(-1));
}
}
/// <summary>
/// raises the event.
/// </summary>
private void HandleTimer()
{
lock (_lock)
{
var ev = this.DelayedEvent;
if (ev != null)
{
DelayedEventArgs args = new DelayedEventArgs(_infoList);
Invoke(()=> ev(this, args));
}
_infoList.Clear();
}
}
private static void ThreadProc(Object stateInfo)
{
EventDelayer thisObj = (EventDelayer)stateInfo;
thisObj.HandleTimer();
}
private static Lazy<System.Windows.Threading.Dispatcher> _dispatchObject = new Lazy<System.Windows.Threading.Dispatcher>(() =>
{
if (Application.Current != null)
{
return Application.Current.Dispatcher;
}
else
{
return null;
}
});
public static void Invoke(Action action)
{
if (_dispatchObject.Value == null || _dispatchObject.Value.CheckAccess())
{
action();
}
else
{
_dispatchObject.Value.Invoke(action);
}
}
}
private class ExampleUsage
{
/// <summary>
/// shows how to create a event delayer and use it to listen to the events from a text box and call if no further changes for 2 seconds.
/// </summary>
private static void ShowUsage(System.Windows.Controls.TextBox textBox)
{
EventDelayer eventDelayer = new EventDelayer(TimeSpan.FromSeconds(2));
textBox.TextChanged += eventDelayer.Enqueue;
eventDelayer.DelayedEvent += eventDelayer_DelayedEvent;
}
/// <summary>
/// redo search here. if required you can access the event args originally raised from the textbox through the event args of this method
/// </summary>
static void eventDelayer_DelayedEvent(object sender, EventDelayer.DelayedEventArgs e)
{
foreach (var eventInfo in e)
{
var originalSender = eventInfo.Sender;
var args = eventInfo.EventArgs;
var timeInitiallyCalled = eventInfo.EventTime;
}
}
}
Bind the textbox text to a string, then set a delay in the binding
<TextBox>
<TextBox.Text>
<Binding Path="searchText" UpdateSourceTrigger="PropertyChanged" Delay="2000" />
</TextBox.Text>
</TextBox>