TabControl SelectionChanged fires multiple times - c#

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.

Related

Custom Pull-To-Refresh not working on iOS

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.

How to create a Editable Spinner (Picker)?

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.

DependencyProperty not notifying UI on NotifyCollectionChanged event

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.

How to expand all nodes of a WPF treeview in code behind?

I might be suffering of Monday's dumbness, but I can't find a nice way of expanding all treeview nodes after I've added them in code behind (something like treeView.ExpandAll()).
Any quick help?
In xaml you could do it as follows :
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="TreeViewItem.IsExpanded" Value="True"/>
</Style>
</TreeView.ItemContainerStyle>
After playing around with all of the various methods for fully expanding and collapsing a tree view, by far the fastest method is the following. This method seems to work on very large trees.
Ensure your tree is virtualized, if it isn't virtualized then as soon as the tree gets to any kind of size it is going to become painfully slow whatever you do.
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
Assume that you have a view model backing your tree, each node on that view model that corresponds to a HierarchicalDataTemplate needs an IsExpanded property (it doesn't need to implement property changed). Assume these view models implement an interface like this:
interface IExpandableItem : IEnumerable
{
bool IsExpanded { get; set; }
}
The TreeViewItem style needs to be set as follows to bind the IsExpanded property in the view model to the view:
<Style
TargetType="{x:Type TreeViewItem}">
<Setter
Property="IsExpanded"
Value="{Binding
IsExpanded,
Mode=TwoWay}" />
</Style>
We are going to use this property to set the expansion state, but also, because the tree is virtualized this property is necessary to maintain the correct view state as the individual TreeViewItems get recycled. Without this binding nodes will get collapsed as they go out of view as the user browses the tree.
The only way to get acceptable speed on large trees is to work in code behind in the view layer. The plan is basically as follows:
Get hold of the current binding to the TreeView.ItemsSource.
Clear that binding.
Wait for the binding to actually clear.
Set the expansion state in the (now unbound) view model.
Rebind the TreeView.ItemsSource using the binding we cached in step 1.
Because we have virtualization enabled, performing a bind on TreeView.ItemsSource turns out to be very fast, even with a large view model. Likewise, when unbound updating the expansion state of the nodes should be very fast. This results in surprisingly fast updates.
Here is some code:
void SetExpandedStateInView(bool isExpanded)
{
var model = this.DataContext as TreeViewModel;
if (model == null)
{
// View model is not bound so do nothing.
return;
}
// Grab hold of the current ItemsSource binding.
var bindingExpression = this.TreeView.GetBindingExpression(
ItemsControl.ItemsSourceProperty);
if (bindingExpression == null)
{
return;
}
// Clear that binding.
var itemsSourceBinding = bindingExpression.ParentBinding;
BindingOperations.ClearBinding(
this.TreeView, ItemsControl.ItemsSourceProperty);
// Wait for the binding to clear and then set the expanded state of the view model.
this.Dispatcher.BeginInvoke(
DispatcherPriority.DataBind,
new Action(() => SetExpandedStateInModel(model.Items, isExpanded)));
// Now rebind the ItemsSource.
this.Dispatcher.BeginInvoke(
DispatcherPriority.DataBind,
new Action(
() => this.TreeView.SetBinding(
ItemsControl.ItemsSourceProperty, itemsSourceBinding)));
}
void SetExpandedStateInModel(IEnumerable modelItems, bool isExpanded)
{
if (modelItems == null)
{
return;
}
foreach (var modelItem in modelItems)
{
var expandable = modelItem as IExpandableItem;
if (expandable == null)
{
continue;
}
expandable.IsExpanded = isExpanded;
SetExpandedStateInModel(expandable, isExpanded);
}
}
WPF doesn't have an ExpandAll method. You'll need to loop through and set the property on each node.
See this question or this blog post.
I have done an ExpandAll that works also if your tree is set for virtualization (recycling items).
This is my code. Perhaps you should consider wrapping your hierarchy into a hierarchical model model view ?
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;
using HQ.Util.General;
namespace HQ.Util.Wpf.WpfUtil
{
public static class TreeViewExtensions
{
// ******************************************************************
public delegate void OnTreeViewVisible(TreeViewItem tvi);
public delegate void OnItemExpanded(TreeViewItem tvi, object item);
public delegate void OnAllItemExpanded();
// ******************************************************************
private static void SetItemHierarchyVisible(ItemContainerGenerator icg, IList listOfRootToNodeItemPath, OnTreeViewVisible onTreeViewVisible = null)
{
Debug.Assert(icg != null);
if (icg != null)
{
if (listOfRootToNodeItemPath.Count == 0) // nothing to do
return;
TreeViewItem tvi = icg.ContainerFromItem(listOfRootToNodeItemPath[0]) as TreeViewItem;
if (tvi != null) // Due to threading, always better to verify
{
listOfRootToNodeItemPath.RemoveAt(0);
if (listOfRootToNodeItemPath.Count == 0)
{
if (onTreeViewVisible != null)
onTreeViewVisible(tvi);
}
else
{
if (!tvi.IsExpanded)
tvi.IsExpanded = true;
SetItemHierarchyVisible(tvi.ItemContainerGenerator, listOfRootToNodeItemPath, onTreeViewVisible);
}
}
else
{
ActionHolder actionHolder = new ActionHolder();
EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
{
var icgSender = sender as ItemContainerGenerator;
tvi = icgSender.ContainerFromItem(listOfRootToNodeItemPath[0]) as TreeViewItem;
if (tvi != null) // Due to threading, it is always better to verify
{
SetItemHierarchyVisible(icg, listOfRootToNodeItemPath, onTreeViewVisible);
actionHolder.Execute();
}
};
actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
icg.StatusChanged += itemCreated;
return;
}
}
}
// ******************************************************************
/// <summary>
/// You cannot rely on this method to be synchronous. If you have any action that depend on the TreeViewItem
/// (last item of collectionOfRootToNodePath) to be visible, you should set it in the 'onTreeViewItemVisible' method.
/// This method should work for Virtualized and non virtualized tree.
/// The difference with ExpandItem is that this one open up the tree up to the target but will not expand the target itself,
/// while ExpandItem expand the target itself.
/// </summary>
/// <param name="treeView">TreeView where an item has to be set visible</param>
/// <param name="listOfRootToNodePath">Any collectionic List. The collection should have every objet of the path to the targeted item from the root
/// to the target. For example for an apple tree: AppleTree (index 0), Branch4, SubBranch3, Leaf2 (index 3)</param>
/// <param name="onTreeViewVisible">Optionnal</param>
public static void SetItemHierarchyVisible(this TreeView treeView, IEnumerable<object> listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
{
ItemContainerGenerator icg = treeView.ItemContainerGenerator;
if (icg == null)
return; // Is tree loaded and initialized ???
SetItemHierarchyVisible(icg, new List<object>(listOfRootToNodePath), onTreeViewVisible);
}
// ******************************************************************
private static void ExpandItem(ItemContainerGenerator icg, IList listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
{
Debug.Assert(icg != null);
if (icg != null)
{
if (listOfRootToNodePath.Count == 0) // nothing to do
return;
TreeViewItem tvi = icg.ContainerFromItem(listOfRootToNodePath[0]) as TreeViewItem;
if (tvi != null) // Due to threading, always better to verify
{
listOfRootToNodePath.RemoveAt(0);
if (!tvi.IsExpanded)
tvi.IsExpanded = true;
if (listOfRootToNodePath.Count == 0)
{
if (onTreeViewVisible != null)
onTreeViewVisible(tvi);
}
else
{
SetItemHierarchyVisible(tvi.ItemContainerGenerator, listOfRootToNodePath, onTreeViewVisible);
}
}
else
{
ActionHolder actionHolder = new ActionHolder();
EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
{
var icgSender = sender as ItemContainerGenerator;
tvi = icgSender.ContainerFromItem(listOfRootToNodePath[0]) as TreeViewItem;
if (tvi != null) // Due to threading, it is always better to verify
{
SetItemHierarchyVisible(icg, listOfRootToNodePath, onTreeViewVisible);
actionHolder.Execute();
}
};
actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
icg.StatusChanged += itemCreated;
return;
}
}
}
// ******************************************************************
/// <summary>
/// You cannot rely on this method to be synchronous. If you have any action that depend on the TreeViewItem
/// (last item of collectionOfRootToNodePath) to be visible, you should set it in the 'onTreeViewItemVisible' method.
/// This method should work for Virtualized and non virtualized tree.
/// The difference with SetItemHierarchyVisible is that this one open the target while SetItemHierarchyVisible does not try to expand the target.
/// (SetItemHierarchyVisible just ensure the target will be visible)
/// </summary>
/// <param name="treeView">TreeView where an item has to be set visible</param>
/// <param name="listOfRootToNodePath">The collection should have every objet of the path, from the root to the targeted item.
/// For example for an apple tree: AppleTree (index 0), Branch4, SubBranch3, Leaf2</param>
/// <param name="onTreeViewVisible">Optionnal</param>
public static void ExpandItem(this TreeView treeView, IEnumerable<object> listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
{
ItemContainerGenerator icg = treeView.ItemContainerGenerator;
if (icg == null)
return; // Is tree loaded and initialized ???
ExpandItem(icg, new List<object>(listOfRootToNodePath), onTreeViewVisible);
}
// ******************************************************************
private static void ExpandSubWithContainersGenerated(ItemsControl ic, Action<TreeViewItem, object> actionItemExpanded, ReferenceCounterTracker referenceCounterTracker)
{
ItemContainerGenerator icg = ic.ItemContainerGenerator;
foreach (object item in ic.Items)
{
var tvi = icg.ContainerFromItem(item) as TreeViewItem;
actionItemExpanded(tvi, item);
tvi.IsExpanded = true;
ExpandSubContainers(tvi, actionItemExpanded, referenceCounterTracker);
}
}
// ******************************************************************
/// <summary>
/// Expand any ItemsControl (TreeView, TreeViewItem, ListBox, ComboBox, ...) and their childs if any (TreeView)
/// </summary>
/// <param name="ic"></param>
/// <param name="actionItemExpanded"></param>
/// <param name="referenceCounterTracker"></param>
public static void ExpandSubContainers(ItemsControl ic, Action<TreeViewItem, object> actionItemExpanded, ReferenceCounterTracker referenceCounterTracker)
{
ItemContainerGenerator icg = ic.ItemContainerGenerator;
{
if (icg.Status == GeneratorStatus.ContainersGenerated)
{
ExpandSubWithContainersGenerated(ic, actionItemExpanded, referenceCounterTracker);
}
else if (icg.Status == GeneratorStatus.NotStarted)
{
ActionHolder actionHolder = new ActionHolder();
EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
{
var icgSender = sender as ItemContainerGenerator;
if (icgSender.Status == GeneratorStatus.ContainersGenerated)
{
ExpandSubWithContainersGenerated(ic, actionItemExpanded, referenceCounterTracker);
// Never use the following method in BeginInvoke due to ICG recycling. The same icg could be
// used and will keep more than one subscribers which is far from being intended
// ic.Dispatcher.BeginInvoke(actionHolder.Action, DispatcherPriority.Background);
// Very important to unsubscribe as soon we've done due to ICG recycling.
actionHolder.Execute();
referenceCounterTracker.ReleaseRef();
}
};
referenceCounterTracker.AddRef();
actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
icg.StatusChanged += itemCreated;
// Next block is only intended to protect against any race condition (I don't know if it is possible ? How Microsoft implemented it)
// I mean the status changed before I subscribe to StatusChanged but after I made the check about its state.
if (icg.Status == GeneratorStatus.ContainersGenerated)
{
ExpandSubWithContainersGenerated(ic, actionItemExpanded, referenceCounterTracker);
}
}
}
}
// ******************************************************************
/// <summary>
/// This method is asynchronous.
/// Expand all items and subs recursively if any. Does support virtualization (item recycling).
/// But honestly, make you a favor, make your life easier en create a model view around your hierarchy with
/// a IsExpanded property for each node level and bind it to each TreeView node level.
/// </summary>
/// <param name="treeView"></param>
/// <param name="actionItemExpanded"></param>
/// <param name="actionAllItemExpanded"></param>
public static void ExpandAll(this TreeView treeView, Action<TreeViewItem, object> actionItemExpanded = null, Action actionAllItemExpanded = null)
{
var referenceCounterTracker = new ReferenceCounterTracker(actionAllItemExpanded);
referenceCounterTracker.AddRef();
treeView.Dispatcher.BeginInvoke(new Action(() => ExpandSubContainers(treeView, actionItemExpanded, referenceCounterTracker)), DispatcherPriority.Background);
referenceCounterTracker.ReleaseRef();
}
// ******************************************************************
}
}
And
using System;
using System.Threading;
namespace HQ.Util.General
{
public class ReferenceCounterTracker
{
private Action _actionOnCountReachZero = null;
private int _count = 0;
public ReferenceCounterTracker(Action actionOnCountReachZero)
{
_actionOnCountReachZero = actionOnCountReachZero;
}
public void AddRef()
{
Interlocked.Increment(ref _count);
}
public void ReleaseRef()
{
int count = Interlocked.Decrement(ref _count);
if (count == 0)
{
if (_actionOnCountReachZero != null)
{
_actionOnCountReachZero();
}
}
}
}
}
You have to include the following method in your project:
private void ExpandAllNodes(TreeViewItem treeItem)
{
treeItem.IsExpanded = true;
foreach (var childItem in treeItem.Items.OfType<TreeViewItem>())
{
ExpandAllNodes(childItem);
}
}
then, you only need to call it like this:
treeView.Items.OfType<TreeViewItem>().ToList().ForEach(ExpandAllNodes);

How can I get all controls from a Form Including controls in any container?

I need, for example, a way to disable all buttons in a form or validate all textboxes' data. Any ideas? Thanks in advance!
The simplest option may be to cascade:
public static void SetEnabled(Control control, bool enabled) {
control.Enabled = enabled;
foreach(Control child in control.Controls) {
SetEnabled(child, enabled);
}
}
or similar; you could of course pass a delegate to make it fairly generic:
public static void ApplyAll(Control control, Action<Control> action) {
action(control);
foreach(Control child in control.Controls) {
ApplyAll(child, action);
}
}
then things like:
ApplyAll(this, c => c.Validate());
ApplyAll(this, c => {c.Enabled = false; });
I prefer a lazy (iterator) approach to the problem, so this is what I use:
/// <summary> Return all of the children in the hierarchy of the control. </summary>
/// <exception cref="ArgumentNullException"> Thrown when one or more required arguments are null. </exception>
/// <param name="control"> The control that serves as the root of the hierarchy. </param>
/// <param name="maxDepth"> (optional) The maximum number of levels to iterate. Zero would be no
/// controls, 1 would be just the children of the control, 2 would include the children of the
/// children. </param>
/// <returns>
/// An enumerator that allows foreach to be used to process iterate all children in this
/// hierarchy.
/// </returns>
public static IEnumerable<Control> IterateAllChildren(this Control control,
int maxDepth = int.MaxValue)
{
if (control == null)
throw new ArgumentNullException("control");
if (maxDepth == 0)
return new Control[0];
return IterateAllChildrenSafe(control, 1, maxDepth);
}
private static IEnumerable<Control> IterateAllChildrenSafe(Control rootControl,
int depth,
int maxDepth)
{
foreach (Control control in rootControl.Controls)
{
yield return control;
// only iterate children if we're not too far deep and if we
// actually have children
if (depth >= maxDepth || control.Controls.Count == 0)
continue;
var children = IterateAllChildrenSafe(control, depth + 1, maxDepth);
foreach (Control subChildControl in children)
{
yield return subChildControl;
}
}
}
Also try:
public List<Control> getControls(string what, Control where)
{
List<Control> controles = new List<Control>();
foreach (Control c in where.Controls)
{
if (c.GetType().Name == what)
{
controles.Add(c);
}
else if (c.Controls.Count > 0)
{
controles.AddRange(getControls(what, c));
}
}
return controles;
}
private void Form1_Load(object sender, EventArgs e)
{
var c = getControls("Button", this);
}
I've been looking for a solution for the same to enable/disable controls based on type,so I came up with this similar to Luiscencio approach (You may also modify it to get all controls or change other properties).
public static void setEnabled (ControlCollection cntrList ,bool enabled,List<Type> typeList = null)
{
foreach (Control cntr in cntrList)
{
if (cntr.Controls.Count == 0)
if (typeList != null)
{
if (typeList.Contains(cntr.GetType()))
cntr.Enabled = enabled;
}
else
cntr.Enabled = enabled;
else
setEnabled(cntr.Controls, enabled, typeList);
}
}
public void loadFormEvents()
{
List<Type> list = new List<Type> ();
list.Add(typeof(TextBox));
setEnabled(frm.Controls ,false,list);
}

Categories

Resources