I'm creating a replacement for ListView that allows the user to rearrange items in the UI.
But if the screen that is using this new ListView wants to know when items are rearranged, it will set the ItemsSource property to an ObservableCollection and then expect to be told when items are rearranged.
XAML:
<Layout>
<MyListView x:Name="MyList">
</Layout>
Code behind:
public class MyScreen
{
ObservableCollection<MyItem> Items = new ObservableCollection<MyItem>();
public MyScreen()
{
InitializeComponent();
MyList.ItemsSource = Items;
Items.CollectionChanged += OnItemsChanged;
}
}
MyListView.cs:
public class MyListView : AbsoluteLayout
{
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(MyListView), null);
public IEnumerable ItemsSource
{
get { return (IEnumerable)this.GetValue(ItemsSourceProperty); }
set { this.SetValue(ItemsSourceProperty, value); }
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (propertyName == nameof(ItemsSource))
{
foreach (object item in ItemsSource)
{
// Create a visual element, and set the BindingContext of that
// visual element to 'item'
}
}
}
public void RearrangeItem(int beforeIndex, int afterIndex)
{
if (ItemsSource is ObservableCollection<ARGH> collection)
{
collection.RemoveAt(...);
collection.Insert(...);
}
}
}
And you see the problem. I can't cast ItemsSource (an IEnumerable for consistency with Xamarin.Forms.ListView) to an ObservableCollection without knowing the type of objects in the collection, and the most straightforward way to do that would be to have the MyListView be generic as well, but since I'm creating the MyListView in XAML I can't do that.
I'm sure I could do some clever stuff with reflection where I ask the ItemsSource whether it derives from any ObservableCollection, and if so find the "RemoveAt" and "Insert" functions and call them using reflection, but I was hoping to find something simpler ...
ObservableCollection<T> implements the non-generic IList interface, so you can cast to that and call RemoveAt() & Insert() passing object arounds.
SLaks answer is the easiest if all you need is RemoveAt() and Insert(); for Move() this is what I ended up with so that I could call ObservableCollection<>.Move(oldIndex, newIndex) on an IEnumerable:
Type t = ItemsSource.GetType();
if (t.Name == "ObservableCollection`1")
{
MethodInfo method = t.GetRuntimeMethod("Move", new Type[] { typeof(Int32), typeof(Int32) });
method.Invoke(ItemsSource, new object[] { oldIndex, newIndex });
}
Related
I have series of listboxes and comboboxes that I want to update, so that the items listed are the same as those listed in a IEnumerable<string>. I do understand that data binding might help, but I think it could be a bit of a struggle now, and I'd rather avoid it.
I wrote something like this:
public static void UpdateListboxWithStrings(
ListControl listcontrol,
IEnumerable<string> stringlist)
{
Object listcontrolitems;
if (listcontrol is ListBox)
{
listcontrolitems =
((ListBox)listcontrol).Items;
}
else if (listcontrol is ComboBox)
{
listcontrolitems =
((ComboBox)listcontrol).Items;
}
else
{
//// Wrong control type.
//// EARLY EXIT
return;
}
int itemscount = listcontrolitems.Count;
/// More code here...
}
... And troubles begin. Depending on what I add / remove, listcontrolitems appears undefined, or must be initialized, or it doesn't have properties such as Count.
How do you write a function that works with a combobox or in a listbox without code duplication?
LE. It is a Windows Application, NET Framework 4.5 using System.Windows.Forms. I want to add / remove items, count, get and set selection. Also, there might be duplicates. So converting to items to strings won't work.
You won't be able to do this in a convenient way, unless you only need the features that are available in the IList type. In that case, you can skip the wrapper described below and just declare the items local variable as IList, assigning it directly to the Items property in each control-type-specific if branch.
If all you needed was the Count property value, you could assign a local int variable in each type-specific branch (i.e. in the if statement blocks).
But you state that you want to actually manipulate the collections. The System.Windows.Forms.ComboBox.Items and System.Windows.Forms.ListBox.Items collections are two completely different, unrelated types. So if you can't use IList, the only way you would be able to share code that manipulates them would be to wrap the collections in a new type that understands both.
For example:
abstract class ListControlItems
{
public abstract int Count { get; }
public abstract int Add(object item);
public abstract void RemoveAt(int index);
// etc.
}
class ListBoxControlItems : ListControlItems
{
private ListBox.ObjectCollection _items;
public ListBoxControlItems(ListBox.ObjectCollection items)
{
_items = items;
}
public override int Count { get { return _items.Count; } }
public override int Add(object item) { return _items.Add(item); }
public override void RemoveAt(int index) { _items.RemoveAt(index); }
// etc.
}
Do the same thing for a ComboBoxControlItems type. Then in your handler, you can create the appropriate abstract type, and use that to manipulate the collections:
public static void UpdateListboxWithStrings(
ListControl listcontrol,
IEnumerable<string> stringlist)
{
ListControlItems items;
if (listcontrol is ListBox)
{
items = new ListBoxControlItems(((ListBox)listcontrol).Items);
}
else if (listcontrol is ComboBox)
{
items = new ComboBoxControlItems(((ComboBox)listcontrol).Items);
}
else
{
//// Wrong control type.
//// EARLY EXIT
return;
}
int itemscount = items.Count;
/// More code here...
}
I set a pivot itemsSource to a OservableCollection property in my ViewModel. When I click a button I want the pivots ItemSource to be bound to another property in the VM of type ObservableCollection. In the Xaml of the Page I set Pivots' ItemsSource once, and I know It is not a good approach to change it from the code-behind on the button click event, but rather only change the collection's content. The problem is the one is of Type1 and the other of Type2. How to do this in the ViewModel?
In your viewmodel you could have the list property be defined as an Object and bind to that, then in your command to switch the collections just set that object to your other list.
Here is a brief example
private Object _list;
private ObservableCollection<Int32> _intList;
private ObservableCollection<String> _stringList;
public Object List
{
get { return _list; }
set
{
_list = value;
RaisePropertyChanged("List");
}
}
public void CommandExecuted()
{
if (ReferenceEquals(_list, _intList))
{
List = _stringList;
}
else
{
List = _intList;
}
}
If you keep the bound Item as an IEnumerable you should should be able to switch the collection and notify of the switch.
private IEnumerable _boundList;
private ObservableCollection _firstCollection;
private ObservableCollection _SecondCollection;
public IEnumerable BoundList
{
get { return _boundList; }
set
{
_boundList = value;
OnPropertyChanged("BoundList");
}
}
public void CommandExecuted()
{
BoundList = _boundList == _firstCollection ? (IEnumerable)_SecondCollection : _firstCollection;
}
I currently have a ComboBox in my Windows Forms Application. In order to specify which values the ComboBox will contain, I set DataSource property of the ComboBox to some array so that ComboBox contains values from that array. I could also use Items.Add() to add new values to ComboBox. However, I want to make sure that ComboBox can be populated with objects of some specific type. So, if I have a class called X, then I want to make it so that only an array of type X can be used as a data source for the ComboBox. Right now, ComboBox accepts objects of type System.Object. How can I achieve it? Is there a property of ComboBox that I need to set to be equal to my data type's name? Or is there an event that will check whether an object added to my ComboBox is of the needed type and will throw an exception if not?
I was thinking of creating a new class as a subtype of ComboBox, and overriding the Add method of Items property so that Add checks whether its argument is of the needed type (not sure if and how I can do it). Even if I do that, there are still other ways to add new values into ComboBox (AddRange, CopyTo, etc.), so I think there should be a more elegant solution to this problem.
If you want to control the type of item that the ComboBox can contain, you could try creating a new class derived form ComboBox, but you'd run into the problem that it still has the ComboBox.ObjectCollection Items property which would still accept any type! And (unfortunately for your idea of overriding) the Add method isn't virtual.
The only practical solution that I could think of would be to abstract the ComboBox somehow. If this isn't shared code, I would recommend just creating a method that you would use to add items to the ComboBox. Something like:
// NOTE: All items that are added to comboBox1 need to be of type `SomeType`.
private void AddItemToComboBox(SomeType item)
{
comboBox1.Items.Add(item);
}
Any attempt to add a non-SomeType object to the ComboBox would be met with a compiler error. Unfortunately, there's no easy way to prevent someone from still adding a non-SomeType item to ComboBox.Items directly.
Again, if this isn't shared code, it shouldn't really be an issue.
You can hide Items property by your
own Items property of custom type which taking as parameter original ItemsCollection
Example class for testing
public class Order
{
public Int32 ID { get; set; }
public string Reference { get; set; }
public Order() { }
public Order(Int32 inID, string inReference)
{
this.ID = inID;
this.Reference = (inReference == null) ? string.Empty : inReference;
}
//Very important
//Because ComboBox using .ToString method for showing Items in the list
public override string ToString()
{
return this.Reference;
}
}
With next class I tried wrap ComboBox's items collection in own type.
Where adding items must be concrete type
Here you can add other methods/properties you need (Remove)
public class ComboBoxList<TCustomType>
{
private System.Windows.Forms.ComboBox.ObjectCollection _baseList;
public ComboBoxList(System.Windows.Forms.ComboBox.ObjectCollection baseItems)
{
_baseList = baseItems;
}
public TCustomType this[Int32 index]
{
get { return (TCustomType)_baseList[index]; }
set { _baseList[index] = value; }
}
public void Add(TCustomType item)
{
_baseList.Add(item);
}
public Int32 Count { get { return _baseList.Count; } }
}
Here custom combobox class derived from ComboBox
Added: generic type
public class ComboBoxCustomType<TCustomType> : System.Windows.Forms.ComboBox
{
//Hide base.Items property by our wrapping class
public new ComboBoxList<TCustomType> Items;
public ComboBoxCustomType() : base()
{
this.Items = new ComboBoxList<TCustomType>(base.Items);
}
public new TCustomType SelectedItem
{
get { return (TCustomType)base.SelectedItem; }
}
}
Next code used in the Form
private ComboBoxCustomType<Order> _cmbCustom;
//this method used in constructor of the Form
private void ComboBoxCustomType_Initialize()
{
_cmbCustom = new ComboBoxCustomType<Order>();
_cmbCustom.Location = new Point(100, 20);
_cmbCustom.Visible = true;
_cmbCustom.DropDownStyle = ComboBoxStyle.DropDownList;
_cmbCustom.Items.Add(new Order(0, " - nothing - "));
_cmbCustom.Items.Add(new Order(1, "One"));
_cmbCustom.Items.Add(new Order(2, "Three"));
_cmbCustom.Items.Add(new Order(3, "Four"));
_cmbCustom.SelectedIndex = 0;
this.Controls.Add(_cmbCustom);
}
Instead of overriding ComboBox (which wont work as stated in itsme86's answer) you could override usercontrol, add a combobox to this, and then only expose the elements that you wish to work with. Something similar to
public partial class MyComboBox<T> : UserControl where T: class
{
public MyComboBox()
{
InitializeComponent();
}
public void Add(T item)
{
comboBox1.Items.Add(item);
}
public IEnumerable<T> Items
{
get { return comboBox1.Items.Cast<T>(); }
}
}
Please note however that some pieces of automated software rely on access the the underlying controls however so this may cause some issues.
This approach never changes the Items of the combobox so they will still store as objects but when you access them, you are casting them to the correct type and only allowing them to be added of that type. You can create a new combobox via
var myCB = new MyComboBox<ItemClass>();
I have following class which I use for radio button binding
public class RadioButtonSwitch : ViewModelBase
{
IDictionary<string, bool> _options;
public RadioButtonSwitch(IDictionary<string, bool> options)
{
this._options = options;
}
public bool this[string a]
{
get
{
return _options[a];
}
set
{
if (value)
{
var other = _options.Where(p => p.Key != a).Select(p => p.Key).ToArray();
foreach (string key in other)
_options[key] = false;
_options[a] = true;
RaisePropertyChanged("XXXX");
else
_options[a] = false;
}
}
}
XAML
<RadioButton Content="Day" IsChecked="{Binding RadioSwitch[radio1], Mode=TwoWay}" GroupName="Monthly" HorizontalAlignment="Left" VerticalAlignment="Center" />
ViewModel
RadioSwitch = new RadioButtonSwitch(
new Dictionary<string, bool> {{"radio1", true},{"radio2", false}}
);
I'm having problem with RaisePropertyChanged() in my Class. I'm not sure what value I should be putting in order to raise the change.
I tried putting:
Item[]
a
[a]
I keep getting following error:
This is so in case of any change I can handle it in my view accordingly. Please do not give me solutions for List for radio buttons etc.
The problem is that you are implementing an indexer, not an ordinary property. Although the binding subsystem supports indexers, MVVMLight and INotifyPropertyChanged do not.
If you want to use an indexer you need to:
Use a collection base class such as ObservableCollection<T>
Implement INotifiyCollectionChanged and raise that event instead
The first option isn't realistic because you are already deriving from ViewModelBase and have to continue to do that. Since implementing INotifiyCollectionChanged is a little bit of work, the easiest approach is to:
add a property to RadioButtonSwitch that is an observable collection of boolean values (ObservableCollection<bool>)
Then change you binding to add one more path element and you are done.
Edit:
Based on your comment and rereading your question, I think implementing INotifyCollectionChanged is the easiest. Here is the rewrite of your RadioButtonSwitch class which actually no longer needs to derive from the MVVMLight base class, although you still could if you wanted to.
The careful reader will notice that we use a sledgehammer and "reset" the whole collection when any element of the collection is modified. This is not just laziness; it is because the indexer uses a string index instead of an integer index and INotifyCollectionChanged doesn't support that. As a result, when anything changes we just throw up our hands and say the whole collection has changed.
public class RadioButtonSwitch : INotifyCollectionChanged
{
public event NotifyCollectionChangedEventHandler CollectionChanged;
protected void RaiseCollectionChanged()
{
if (CollectionChanged != null)
CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
IDictionary<string, bool> _options;
public RadioButtonSwitch(IDictionary<string, bool> options)
{
this._options = options;
}
public bool this[string a]
{
get
{
return _options[a];
}
set
{
if (value)
{
var other = _options.Where(p => p.Key != a).Select(p => p.Key).ToArray();
foreach (string key in other)
_options[key] = false;
_options[a] = true;
RaiseCollectionChanged();
}
else
_options[a] = false;
}
}
}
GalaSoft.MvvmLight has the following code to check property name before raising PropertyChanged event.
public void VerifyPropertyName(string propertyName)
{
if (GetType().GetProperty(propertyName) == null)
throw new ArgumentException("Property not found", propertyName);
}
GetType().GetProperty("Item[]") obviously returns null.
This is why it is failing.
I think, the quickest workaround for you would be not to use ViewModelBase from this library, but implement your own version, that doesn't do this check:
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
If you implement this class you will be able to run RaisePropertyChanged("Item[]").
I'm trying to get a binding to work on a child object of a user control. The Xaml looks like this:
<MyGrid>
<MyColumn ExtendedColumnData="{Binding ColumnToolTipDescriptions}"/>
</MyGrid>
Here is how the classes are defined:
[ContentProperty("Columns")]
public class MyGrid : UserControl
{
private MyColumnCollection _columns;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content), Category("Data")]
public MyColumnCollection Columns
{
get
{
if (_columns == null)
_columns = new MyColumnCollection();
return _columns;
}
}
}
public class MyColumnCollection : ObservableCollection<MyGridColumn>
{
}
public class MyGridColumn : DependencyObject
{
public object ExtendedColumnData
{
get { return (object)GetValue(ExtendedColumnDataProperty); }
set { SetValue(ExtendedColumnDataProperty, value); }
}
public static readonly DependencyProperty ExtendedColumnDataProperty =
DependencyProperty.Register("ExtendedColumnData", typeof(object), typeof(MyGridColumn), new UIPropertyMetadata(null));
}
From what I can tell, the binding is not even attempting to get the data as I've tried putting a converter against the binding, and the breakpoint on the Convert method never gets hit.
I'm using the MVVM pattern so the window's DataContext property is set to a view model.
I've read some other questions on here and tried various permutations of the binding such as:
<MyColumn ExtendedColumnData="{Binding DataContext.ColumnToolTipDescriptions, ElementName=MyViewName}" />
<MyColumn ExtendedColumnData="{Binding DataContext.ColumnToolTipDescriptions, RelativeSource={RelativeSource AncestorType={x:Type local:MyView}}" />
But still no luck, the binding doesn't fire! The annoying thing is, this seems to work fine (if I add the property to the grid):
<MyGrid ExtendedColumnData="{Binding ColumnToolTipDescriptions}">
<MyColumn />
</MyGrid>
I'm not that experienced with WPF so I'm sure I'm missing something?
The problem is that MyColumnCollection is not inheriting data context (usual properties of a control are not part of inheritance context). If you don't have a data context bindings will not work.
To fix that, try inheriting MyColumnCollection not from ObservableCollection, but from FreezableCollection (freezable properties are part of inheritance context).
The problem is logical tree. The direct solution should be (adapted to your example):
public MyColumnCollection Columns
{
get
{
if (_columns == null)
{
_columns = new MyColumnCollection();
_columns.CollectionChanged += Columns_CollectionChanged;
}
return _columns;
}
}
private void Columns_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
AddLogicalChild(item);
}
}
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
RemoveLogicalChild(item);
}
}
// not sure about Action == reset
}
protected override IEnumerator LogicalChildren
{
get
{
return _columns == null ? null : _columns.GetEnumerator();
}
}
to maintain the logical tree of controls in order for dataContext to be propagated to children. When you call AddLogicalChild, it marks MyGrid as containing logical children, then LogicalChildren will be read and the dataContext of those children will be set (you can listen to DataContextChanged event in them). Overriding LogicalChildren is essential because FrameworkElement doesn't keep the list of children through AddLogicalChild and RemoveLogicalChild calls, oddly.