custom control binding with ObservableCollection - c#

I have custom control MyGrid
public class MyGrid : Canvas
{
//...
ObservableCollection<object> items = new ObservableCollection<object>();
public ObservableCollection<object> Items
{
get { return items; }
set {
items = value;
UpdateValues();
UpdateGrid();
}
}
//..
}
And I want Items to be bindable from XAML code:
<local:MyGrid Items="{Binding Numbers}" />
Where Numbers is ObservableCollection (which works fine, I can use it to bind to default controls).
I've tried to define Items as DependencyProperty, but it is static and I need to use more than one control on page with different sources of data, so using static items won't work. The code above doesn't work as well. InitializeComponent() throws an exception: Failed to assign to property 'App.MyGrid.Items'. [Line: 27 Position: 114]. How can I make it work?

As your MyGrid extends from Canvas (Which is long last also a DependecyObject) you could implement the Dependency property inside the MyGrid.
You could then also implement it with a PropertyChangedCallback which would allow you to register/unregister to the event itself, where you could then update your grid / values
So you could change the MyGrid like this:
public class MyGrid : Canvas
{
protected static PropertyChangedCallback ItemsPropertyChangedCallback = new PropertyChangedCallback(ItemsPropertyChanged);
public static DependencyProperty ItemsProperty = DependencyProperty.RegisterAttached("Items", typeof(INotifyCollectionChanged), typeof(MyGrid), new PropertyMetadata(null, ItemsPropertyChangedCallback));
private static void ItemsPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
MyGrid thisGrid = (MyGrid)sender;
if (thisGrid == null)
{
return;
}
thisGrid.UnregisterItems(e.OldValue as INotifyCollectionChanged);
thisGrid.RegisterItems(e.NewValue as INotifyCollectionChanged);
thisGrid.Refresh();
}
public INotifyCollectionChanged Items
{
get
{
return (INotifyCollectionChanged)GetValue(ItemsProperty);
}
set
{
SetValue(ItemsProperty, value);
}
}
protected void UnregisterItems(INotifyCollectionChanged items)
{
if (items == null)
{
return;
}
items.CollectionChanged -= ItemsChanged;
}
protected void RegisterItems(INotifyCollectionChanged items)
{
if (items == null)
{
return;
}
items.CollectionChanged += ItemsChanged;
}
protected virtual void UpdateValues()
{
System.Diagnostics.Debug.WriteLine("Updating values");
}
protected virtual void UpdateGrid()
{
System.Diagnostics.Debug.WriteLine("Updating grid");
}
public void Refresh()
{
UpdateValues();
UpdateGrid();
}
protected virtual void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
Refresh();
}
public MyGrid()
{
}
}
and in Xaml you could then later on Bind to the Items property. When the Items property is changed with another collection, it will unregister from the changed event of the last object (if there was one), and then register to the new object (if there is one). Afterwards, it will call the Refresh method of your class (that then calls the UpdateValues / UpdateGrid methods)
I also partly agree with #user3248647 that you should take advantage of Binding and ContentTemplates when you can, but if you cannot use that, you could get your DependencyProperty reacting at least like this.
And yes, the DependencyProperty is static on the class, but the property itself is always implemented inside the class. When using the PropertyChangedCallback, just cast the sender back to "MyGrid" and then you can change the instance members :)

May be this will help you.
<ItemsControl ItemsSource="{Binding items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,5,5,0" VerticalAlignment="Top">
<TextBlock TextAlignment="Right" FontWeight="Bold" Text="{Binding yourVariable}" Height="16"/>
<TextBlock TextAlignment="Right" Text="{Binding yourVariable1}" FontSize="26"/>
<TextBlock TextAlignment="Right" Text="{Binding yourVariable2}" FontSize="10" Foreground="DarkGray"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer Padding="{TemplateBinding Padding}" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel>
</WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
This may be not actually what you looking for. But I think this can help you.

Related

MuliSelectCombobox in WPF/C# to show Displaynames of all selected Items

A Combobox binds to a list of custom combobox items where each Item contains a checkbox together with an Image. The user can click either the image or the checkbox to select the item.
Each item contains a relative image path, the Selection-State. The viewmodel generates the list CustomCheckboxItems which the view then binds to.
Now i want to show the Displaynames of all selected items in the Combobox as ... selected items ... together. How can i achieve that? I tried to attach a contentpresenter to the combobox without success since i do not know exactly where to attach it to. Also writing control templates did not do the trick for me.
In the end the combobox should look something like this (Link goes to cdn.syncfusion.com). The ViewModel contains already a comma separated string containing the selected items. How do i have to change the combobox to behave like this?
View:
<ComboBox ItemsSource="{Binding Path=ViewModel.CustomCheckBoxItems, Mode=OneTime}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type viewModels:CustomCheckBoxItem}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=ImagePath}">
<Image.InputBindings>
<MouseBinding Gesture="LeftClick" Command="{Binding SelectItem, Mode=OneWay}" />
</Image.InputBindings>
</Image>
<CheckBox VerticalAlignment="Center" VerticalContentAlignment="Center"IsChecked="Binding Selected, Mode=TwoWay}" >
<TextBlock Text="{Binding DisplayName}" IsEnabled="False" VerticalAlignment="Center" />
</CheckBox>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
The CustomCheckBoxItem Implementation
//INotifyPropertryChanged implementation and other things excluded for brevity
public class CustomCheckBoxItem : INotifyPropertyChanged
{
public CheckboxItem(ItemType item, string imagePath)
{
Item = item;
try{ImagePath = "/" + currentAssemblyName + ";component/Images/" + imagePath;}catch(Exception){}
}
private bool selected;
public bool Selected
{
get => selected;
set
{
selected = value;
NotifyPropertyChanged();
}
}
public ICommand SelectItem => new RelayCommand(() =>
{
if (isInit)
{
Selected = !selected;
}
},false);
public string ImagePath { get; }
public string DisplayName => Item.GetDisplayName();
}
The solution is to disconnect the ContentPresenter, that hosts the selected item in the selection box, from the ComboBox.
The ComboBox sets the selection box value internally. There is no chance to modify this behavior. Since the selection box also serves as input field for the edit or search mode, this logic is quite complex. From this perspective, the design choice to make the complete related logic of the ComboBox internal makes much sense.
The first option is to simply override the default template of the ComboBox and remove the bindings on the ContentPresenter.ContentTemplate, ContentPresenter.ContentTemplateSelector and ContentPresenter.ContentStringFormat properties.
Then bind the ContentPresenter.Content property to the data source that holds the multi selection display names. The disadvantage is that the complete multi-select logic is outside the ComboBox (in the worst case, it even bleeds into the view model). As the multi-select behavior is part of the control, it should be implemented inside.
The second solution is to extend ComboBox to make the handling of the control more convenient and reusable. You probably don't want your view model to be responsible to participate in the multi-select logic e.g. by tracking selection states in the context of multi-select (which is not of interest from the view model's point of view) and by exposing the display values to represent the multi-select state. You view model should not care about display values or multi-select.
The third option is to implement your custom ComboBox and let it extend MultiSelector. If you require all the ComboBox features, then this solutions can take a while to implement and test properly. If you only need a basic multi-select dropdown control, then the task is quite simple and the cleanest solution.
Other options you may find, like hacking the edit mode to set the ComboBox.Text property, are not recommended as they interfere with the internal behavior: the logic around the IsEditable and IsReadOnly is quite complex. So better don't mess around with to prevent unexpected behavior.
Also, you lose many features like search and edit. If you don't care about those feature, then the best and cleanest solution is the third introduced option to implement a Custom control that extends MultiSelector.
Implementation
The following example implements the second solution. The MultiSelectComboBox extends ComboBox. It internally grabs the ContentPresenter, disconnects it from the ComboBox (by overriding its content and removing the internal templating) and tracks the selection state. The advantage of this solution is that you don't have to "hack" the edit mode of the ComboBox. Therefore, the edit mode feature remains untouched. This solution simply changes the displayed values in the default toggle box. Even the default single.select behavior remains intact.
The multi-select state is realized by overriding the ComboBoxItem template to replace the ContentPresenter with a ToggleButton or alternatively, create a ComboBoxToggleItem (like in the example below) that extends ComboBoxItem to make everything reusable and MVVM ready.
Additionally we need to introduce a custom IsItemSelected property by implementing it as an attached property. This is necessary because the ComboBoxItem.IsSelected property is controlled by the Selector (the superclass of ComboBox). And by making it attached, we can avoid a tight coupling between the MultiSelectComboBox logic and the ComboBoxToggleItem Everything still works with the default ComboBoxItem or any other item container. The Selector is also responsible to ensure that only a single item is selected. But we need multiple items to be selected simultaneously.
This way we can easily track selected items and expose them via a public SelectedItems property.
You can use the ItemContainerStyle to bind the data model's selection property (if existing) to this attached MultiSelectComboBox.IsItemSelected property.
By implementing a custom ComboBoxItem like the ComboBoxToggleItem and hard-coding the CheckBox into its ControlTemplate you are no longer forced to track the visual state in your view model. This offers a clean separation. The visibility of the CheckBox in this example is able to be toggled by handling the ComboBoxToggleItem.IsCheckBoxEnabled property.
Because the ComboBoxToggleItem is basically a ToggleButton, you can select the item without clicking the CheckBox. The CheckBox is now an optional feature that only serves to provide another visual feedback.
If you don't define the ItemTemplate, you can control the displayed value using the common DisplayMemberPath (ComboBox default behavior). The MultiSelectComboBox will pick the value from the designated member and concatenate it with the other selected values.
If you want to display different values for the drop-down panel and the selected content box, use the MultiSelectComboBox.SelectionBoxDisplayMemberPath property to specify the item's source property name (in the same manner like the DisplayMemberPath).
If you don't set neither DisplayMemberPath nor the SelectionBoxDisplayMemberPath, the MultiSelectComboBox will call object.ToString on the data item. This way you can even generate a computed value by overriding ToString on the model. This gives you three options to control the selection box display value, while the MultiSelectComboBox concatenates and displays them.
This way the complete logic that handles the displayed values was moved from view model to the view, where it belongs:
MultiSelectComboBox.cs
public class MultiSelectComboBox : ComboBox
{
public static void SetIsItemSelected
(UIElement attachedElement, bool value)
=> attachedElement.SetValue(IsItemSelectedProperty, value);
public static bool GetIsItemSelected(UIElement attachedElement)
=> (bool)attachedElement.GetValue(IsItemSelectedProperty);
public static readonly DependencyProperty IsItemSelectedProperty =
DependencyProperty.RegisterAttached(
"IsItemSelected",
typeof(bool),
typeof(MultiSelectComboBox),
new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnIsItemSelectedChanged));
public string SelectionBoxDisplayMemberPath
{
get => (string)GetValue(SelectionBoxDisplayMemberPathProperty);
set => SetValue(SelectionBoxDisplayMemberPathProperty, value);
}
public static readonly DependencyProperty SelectionBoxDisplayMemberPathProperty = DependencyProperty.Register(
"SelectionBoxDisplayMemberPath",
typeof(string),
typeof(MultiSelectComboBox),
new PropertyMetadata(default));
public IList<object> SelectedItems
{
get => (IList<object>)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(IList<object>),
typeof(MultiSelectComboBox),
new PropertyMetadata(default));
private static Dictionary<DependencyObject, ItemsControl> ItemContainerOwnerTable { get; }
private ContentPresenter PART_ContentSite { get; set; }
private Dictionary<UIElement, string> SelectionBoxContentValues { get; }
static MultiSelectComboBox() => MultiSelectComboBox.ItemContainerOwnerTable = new Dictionary<DependencyObject, ItemsControl>();
public MultiSelectComboBox()
{
this.SelectionBoxContentValues = new Dictionary<UIElement, string>();
this.SelectedItems = new List<object>();
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (TryFindVisualChildElement(this, out ContentPresenter contentPresenter, false))
{
contentPresenter.ContentTemplate = null;
contentPresenter.ContentStringFormat = null;
contentPresenter.ContentTemplateSelector = null;
this.PART_ContentSite = contentPresenter;
}
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
this.SelectedItems.Clear();
MultiSelectComboBox.ItemContainerOwnerTable.Clear();
Dispatcher.InvokeAsync(InitializeSelectionBox, DispatcherPriority.Background);
}
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
foreach (var item in e.OldItems)
{
var itemContainer = this.ItemContainerGenerator.ContainerFromItem(item);
MultiSelectComboBox.ItemContainerOwnerTable.Remove(itemContainer);
this.SelectedItems.Remove(item);
}
break;
}
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
this.SelectionBoxContentValues.Clear();
IEnumerable<(object Item, ComboBoxItem? ItemContainer)>? selectedItemInfos = this.ItemContainerGenerator.Items
.Select(item => (Item: item, ItemContainer: this.ItemContainerGenerator.ContainerFromItem(item) as ComboBoxItem))
.Where(selectedItemInfo => GetIsItemSelected(selectedItemInfo.ItemContainer));
foreach (var selectedItemInfo in selectedItemInfos)
{
string memberPath = this.SelectionBoxDisplayMemberPath
?? this.DisplayMemberPath
?? selectedItemInfo.Item.ToString();
string itemDisplayValue = selectedItemInfo.Item.GetType().GetProperty(memberPath).GetValue(selectedItemInfo.Item)?.ToString()
?? selectedItemInfo.Item.ToString();
this.SelectionBoxContentValues.Add(selectedItemInfo.ItemContainer, itemDisplayValue);
MultiSelectComboBox.ItemContainerOwnerTable.TryAdd(selectedItemInfo.ItemContainer, this);
this.SelectedItems.Add(selectedItemInfo.Item);
}
UpdateSelectionBox();
}
protected override bool IsItemItsOwnContainerOverride(object item) => item is ComboBoxToggleItem;
protected override DependencyObject GetContainerForItemOverride() => new ComboBoxToggleItem();
private static void OnIsItemSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var comboBoxItem = d as ComboBoxItem;
if (MultiSelectComboBox.ItemContainerOwnerTable.TryGetValue(comboBoxItem, out ItemsControl owner))
{
var comboBoxItemOwner = owner as MultiSelectComboBox;
bool isUnselected = !GetIsItemSelected(comboBoxItem);
if (isUnselected)
{
comboBoxItemOwner.SelectionBoxContentValues.Remove(comboBoxItem);
comboBoxOwner.SelectedItems.Remove(comboBoxItem);
UpdateSelectionBox()
}
}
}
private static void UpdateSelectionBox()
{
string selectionBoxContent = string.Join(", ", this.SelectionBoxContentValues.Values);
if (this.IsEditable)
{
this.Text = selectionBoxContent;
}
else
{
this.PART_ContentSite.Content = selectionBoxContent;
}
}
private void OnItemUnselected(object sender, SelectionChangedEventArgs e)
{
foreach (var removedItem in e.RemovedItems)
{
this.SelectedItems.Remove(removedItem);
}
}
private void InitializeSelectionBox()
{
EnsureItemsLoaded();
UpdateSelectionBox();
}
private void EnsureItemsLoaded()
{
IsDropDownOpen = true;
IsDropDownOpen = false;
}
private static bool TryFindVisualChildElement<TChild>(DependencyObject parent,
out TChild resultElement,
bool isTraversingPopup = true)
where TChild : FrameworkElement
{
resultElement = null;
if (isTraversingPopup
&& parent is Popup popup)
{
parent = popup.Child;
if (parent == null)
{
return false;
}
}
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is TChild frameworkElement)
{
resultElement = frameworkElement;
return true;
}
if (TryFindVisualChildElement(childElement, out resultElement, isTraversingPopup))
{
return true;
}
}
return false;
}
}
ComboBoxToggleItem.cs
public class ComboBoxToggleItem : ComboBoxItem
{
public bool IsCheckBoxEnabled
{
get => (bool)GetValue(IsCheckBoxEnabledProperty);
set => SetValue(IsCheckBoxEnabledProperty, value);
}
public static readonly DependencyProperty IsCheckBoxEnabledProperty = DependencyProperty.Register(
"IsCheckBoxEnabled",
typeof(bool),
typeof(ComboBoxToggleItem),
new PropertyMetadata(default));
static ComboBoxToggleItem()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ComboBoxToggleItem), new FrameworkPropertyMetadata(typeof(ComboBoxToggleItem)));
}
// Add text search selection support
protected override void OnSelected(RoutedEventArgs e)
{
base.OnSelected(e);
MultiSelectComboBox.SetIsItemSelected(this, true);
}
}
Generic.xaml
<Style TargetType="local:ComboBoxToggleItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:ComboBoxToggleItem">
<ToggleButton x:Name="ToggleButton"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(local:MultiSelectComboBox.IsItemSelected)}">
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding ElementName=ToggleButton, Path=IsChecked}"
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsCheckBoxEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" />
<ContentPresenter />
</StackPanel>
</ToggleButton>
<ControlTemplate.Triggers>
<Trigger SourceName="ToggleButton"
Property="IsChecked"
Value="True">
<Setter Property="IsSelected"
Value="True" />
</Trigger>
<Trigger SourceName="ToggleButton"
Property="IsChecked"
Value="False">
<Setter Property="IsSelected"
Value="False" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Usage example
DataItem.cs
class DataItem : INotifyPropertyChanged
{
string TextData { get; }
int Id { get; }
bool IsActive { get; }
}
MainViewModel
class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<DataItem> DataItems { get; }
}
MainWIndow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<local:MultiSelectComboBox ItemsSource="{Binding DataItems}"
DisplayMemberPath="TextData"
SelectionBoxDisplayMemberPath="Id">
<local:MultiSelectComboBox.ItemContainerStyle>
<Style TargetType="local:ComboBoxToggleItem">
<Setter Property="local:MultiSelectComboBox.IsItemSelected"
Value="{Binding IsActive}" />
<Setter Property="IsCheckBoxEnabled"
Value="True" />
</Style>
</local:MultiSelectComboBox.ItemContainerStyle>
<local:MultiSelectComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:DataItem}">
<TextBlock Text="{Binding TextData}" />
</DataTemplate>
</local:MultiSelectComboBox.ItemTemplate>
</local:MultiSelectComboBox>
</Window>
If I am understanding correctly:
You just want all the checked items to be moved to top of combobox
You want comma separated list of selected items shown in closed combobox.
If that is correct, the solution is much simpler than you are thinking.
All that is needed is this:
/// <summary>
/// Sort the items in the list by selected
/// </summary>
private void cmb_DropDownOpened ( object sender, EventArgs e )
=> cmb.ItemsSource = CustomCheckBoxItems
.OrderByDescending ( c => c.Selected )
.ThenBy ( c => c.DisplayName );
/// <summary>
/// Display comma separated list of selected items
/// </summary>
private void cmb_DropDownClosed ( object sender, EventArgs e )
{
cmb.IsEditable = true;
cmb.IsReadOnly = true;
cmb.Text = string.Join ( ",", CustomCheckBoxItems.Where ( c => c.Selected )
.OrderBy ( c => c.DisplayName )
.Select ( c => c.DisplayName )
.ToList () );
}
The key to the solution is just to reorder the list every time the combo is opened you present a re-sorted list.
Every time it is closed, you gather selected and show.
Complete working example:
Given this XAML:
<ComboBox Name="cmb" DropDownOpened="cmb_DropDownOpened">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox VerticalAlignment="Center" VerticalContentAlignment="Center" IsChecked="{Binding Selected, Mode=TwoWay}" >
<TextBlock Text="{Binding DisplayName}" IsEnabled="False" VerticalAlignment="Center" />
</CheckBox>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
and this class (removed notify for this sample, since not needed)
public class CustomCheckBoxItem
{
public string Item { get; set; }
private const string currentAssemblyName = "samplename";
public CustomCheckBoxItem ( string item, string imagePath )
{
Item = item;
try
{ ImagePath = "/" + currentAssemblyName + ";component/Images/" + imagePath; }
catch ( Exception ) { }
}
public bool Selected { get; set; }
public string ImagePath { get; }
public string DisplayName => Item;
}
Then to test I just used this:
public ObservableCollection<CustomCheckBoxItem> CustomCheckBoxItems { get; set; }
= new ObservableCollection<CustomCheckBoxItem> ()
{
new ( "Item 1", "image1.png" ),
new ( "Item 2", "image2.png" ),
new ( "Item 3", "image3.png" ),
new ( "Item 4", "image4.png" ),
new ( "Item 5", "image5.png" ),
new ( "Item 6", "image6.png" ),
new ( "Item 7", "image7.png" ),
};
public MainWindow ()
{
InitializeComponent ();
cmb.ItemsSource = CustomCheckBoxItems;
}
/// <summary>
/// Sort the items in the list by selected
/// </summary>
private void cmb_DropDownOpened ( object sender, EventArgs e )
=> cmb.ItemsSource = CustomCheckBoxItems
.OrderByDescending ( c => c.Selected )
.ThenBy ( c => c.DisplayName );
/// <summary>
/// Display comma separated list of selected items
/// </summary>
private void cmb_DropDownClosed ( object sender, EventArgs e )
{
cmb.IsEditable = true;
cmb.IsReadOnly = true;
cmb.Text = string.Join ( ",", CustomCheckBoxItems.Where ( c => c.Selected )
.OrderBy ( c => c.DisplayName )
.Select ( c => c.DisplayName )
.ToList () );
}
Inspired by the post linked in the comments, I wondered how far I'd get using the base WPF building blocks, but without messing with the ComboBox' template itself. The answer from Heena uses a WrapPanel to show a (filtered) SelectedItems collection that updates on the fly.
<ListBox Background="Transparent" IsHitTestVisible="False" BorderBrush="Transparent" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Disabled" BorderThickness="0" ItemsSource="{Binding ElementName=lst,Path=SelectedItems}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ContentData}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I figured a DataTemplateSelector, to differentiate between the SelectedItemTemplate and the DropdownItemsTemplate, in combination with a CollectionView, that has a filter applied, should get us there.
CheckBoxItem.cs
public class CheckboxItem : ViewModelBase
{
public CheckboxItem(string name, string imagePath)
{
DisplayName = name;
ImagePath = imagePath;
SelectItem = new DummyCommand();
}
public string ImagePath { get; }
public string DisplayName { get; }
public ICommand SelectItem { get; }
private bool selected;
public bool Selected
{
get => selected;
set
{
selected = value;
OnPropertyChanged();
ItemSelectionChanged?.Invoke(this, new EventArgs());
}
}
public event EventHandler ItemSelectionChanged;
}
MainViewModel.cs
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
Items = new ObservableCollection<CheckboxItem>
{
new CheckboxItem("smile", "/Images/Smiley.png"),
new CheckboxItem("wink", "/Images/Wink.png"),
new CheckboxItem("shocked", "/Images/Shocked.png"),
new CheckboxItem("teeth", "/Images/Teeth.png"),
};
SelectedItems = (CollectionView)new CollectionViewSource { Source = Items }.View;
SelectedItems.Filter = o => o is CheckboxItem item && item.Selected;
foreach(var item in Items)
{
item.ItemSelectionChanged += (_, _) => SelectedItems.Refresh();
}
// Set a (dummy) SelectedItem for the TemplateSelector to kick in OnLoad.
SelectedItem = Items.First();
}
public ObservableCollection<CheckboxItem> Items { get; }
public CollectionView SelectedItems { get; }
public CheckboxItem SelectedItem { get; }
}
ComboBoxTemplateSelector.cs
public class ComboBoxTemplateSelector : DataTemplateSelector
{
public DataTemplate SelectedItemTemplate { get; set; }
public DataTemplate DropdownItemsTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var itemToCheck = container;
// Search up the visual tree, stopping at either a ComboBox or a ComboBoxItem (or null).
// This will determine which template to use.
while (itemToCheck is not null and not ComboBox and not ComboBoxItem)
itemToCheck = VisualTreeHelper.GetParent(itemToCheck);
// If you stopped at a ComboBoxItem, you're in the dropdown.
return itemToCheck is ComboBoxItem ? DropdownItemsTemplate : SelectedItemTemplate;
}
}
MainWindow.xaml
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
Title="MultiSelectComboBox" Height="350" Width="400">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<local:ComboBoxTemplateSelector x:Key="ComboBoxTemplateSelector">
<local:ComboBoxTemplateSelector.SelectedItemTemplate>
<DataTemplate>
<ListBox ItemsSource="{Binding DataContext.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ComboBox}}}"
BorderThickness="0" IsHitTestVisible="False" BorderBrush="Transparent" Background="Transparent"
ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:CheckboxItem}">
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</local:ComboBoxTemplateSelector.SelectedItemTemplate>
<local:ComboBoxTemplateSelector.DropdownItemsTemplate>
<DataTemplate DataType="{x:Type local:CheckboxItem}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=ImagePath}" Height="50">
<Image.InputBindings>
<MouseBinding Gesture="LeftClick" Command="{Binding SelectItem, Mode=OneWay}" />
</Image.InputBindings>
</Image>
<CheckBox VerticalAlignment="Center" VerticalContentAlignment="Center" Margin="20 0"
IsChecked="{Binding Selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" >
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" />
</CheckBox>
</StackPanel>
</DataTemplate>
</local:ComboBoxTemplateSelector.DropdownItemsTemplate>
</local:ComboBoxTemplateSelector>
</Window.Resources>
<Grid>
<ComboBox ItemsSource="{Binding Path=Items}"
SelectedItem="{Binding SelectedItem, Mode=OneTime}"
ItemTemplateSelector="{StaticResource ComboBoxTemplateSelector}"
Height="30" Width="200" Margin="0 30 0 0"
VerticalAlignment="Top" HorizontalAlignment="Center">
</ComboBox>
</Grid>
</Window>
The ViewModel contains already a comma separated string containing the selected items. How do i have to change the combobox to behave like this?
You could swap
<local:ComboBoxTemplateSelector.SelectedItemTemplate>
<DataTemplate>
<ListBox ItemsSource="{Binding DataContext.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ComboBox}}}"
BorderThickness="0" IsHitTestVisible="False" BorderBrush="Transparent" Background="Transparent"
ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:CheckboxItem}">
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</local:ComboBoxTemplateSelector.SelectedItemTemplate>
With
<local:ComboBoxTemplateSelector.SelectedItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DataContext.CommaSeperatedString, RelativeSource={RelativeSource AncestorType={x:Type ComboBox}}}" />
</DataTemplate>
</local:ComboBoxTemplateSelector.SelectedItemTemplate>
You could also do without the CommaSeperatedString property and combine the CollectionView with Greg M's string.Join approach inside a converter:
<local:ComboBoxTemplateSelector.SelectedItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DataContext.SelectedItems,
RelativeSource={RelativeSource AncestorType={x:Type ComboBox}},
Converter={StaticResource ItemsToCommaSeparatedStringConverter}}" />
</DataTemplate>
</local:ComboBoxTemplateSelector.SelectedItemTemplate>
Resources:
WrapPanel idea:
CheckComboBox : How to prevent a combobox from closing after a selection is clicked?
Using different templates:
Can I use a different Template for the selected item in a WPF ComboBox than for the items in the dropdown part?
Filtering ItemsSource:
custom search for combobox
Smileys:
https://pngimg.com/images/miscellaneous/smiley

Pass ObservableCollection<> type as dependency property

I am trying to create a multi-select Combobox Custom control, This custom control should expose a dependency property called DropDownDataSource through which the user of the control can decide what day should bound to ComboBox. My code looks like this:
MainPage.Xaml
<Grid>
<local:CustomComboBox x:Name="customcb" DropDownDataSource="{x:Bind DropDownDataSource, Mode=OneWay}" Loaded="CustomControl_Loaded"> </local:CustomComboBox>
</Grid>
MainPage.Xaml.cs
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
private ObservableCollection<Item> _dropDownDataSource;
public ObservableCollection<Item> DropDownDataSource
{
get => _dropDownDataSource;
set
{
_dropDownDataSource = value;
OnPropertyChanged();
}
}
public MainPage()
{
this.InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
private void CustomControl_Loaded(object sender, RoutedEventArgs e)
{
var Items = new ObservableCollection<Item>(Enumerable.Range(1, 10)
.Select(x => new Item
{
Text = string.Format("Item {0}", x),
IsChecked = x == 40 ? true : false
}));
DropDownDataSource = Items;
}
}
Models
public class Item : BindableBase
{
public string Text { get; set; }
bool _IsChecked = default;
public bool IsChecked { get { return _IsChecked; } set { SetProperty(ref _IsChecked, value); } }
}
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void SetProperty<T>(ref T storage, T value,
[System.Runtime.CompilerServices.CallerMemberName] String propertyName = null)
{
if (!object.Equals(storage, value))
{
storage = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
protected void RaisePropertyChanged([System.Runtime.CompilerServices.CallerMemberName] String propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
CustomUserControl XAML
<Grid x:Name="GrdMainContainer">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBox Width="200" FontSize="24" Text="{Binding Header, Mode=TwoWay}"
IsReadOnly="True" TextWrapping="Wrap" MaxHeight="200" />
<ScrollViewer VerticalScrollBarVisibility="Auto" MaxHeight="200" Width="200" Background="White">
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Text}"
FontSize="24"
Foreground="Black"
IsChecked="{Binding IsChecked, Mode=TwoWay}"
IsThreeState="False" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</Grid>
CustomUserControl Cs file
public sealed partial class CustomComboBox : UserControl
{
public CustomComboBox()
{
this.InitializeComponent();
}
public ObservableCollection<Item> DropDownDataSource
{
get { return (ObservableCollection<Item>)GetValue(DropDownDataSourceProperty); }
set { SetValue(DropDownDataSourceProperty, value); }
}
public static readonly DependencyProperty DropDownDataSourceProperty =
DependencyProperty.Register("DropDownDataSource", typeof(ObservableCollection<Item>), typeof(CustomComboBox), new PropertyMetadata("", HasDropDownItemUpdated));
private static void HasDropDownItemUpdated(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomComboBox ucrcntrl)
{
var grd = UIElementExtensions.FindControl<Grid>(ucrcntrl, "GrdMainContainer");
grd.DataContext = ucrcntrl.DropDownDataSource as ObservableCollection<Item>;
}
}
}
All looks good to me, but for some reason, Dropdown is coming empty. Instead of the dependency property, If I assign a view model directly to the Control it works fine. But in my condition, it is mandatory that I have properties like DataSource,SelectedIndex, etc on the user control for the end-user to use. Can anyone point out what is going wrong here?
Here, I have attached a copy of my complete code.
I downloaded your sample code, the problem should be in the binding.
<ItemsControl ItemsSource="{Binding Items}">
This way of writing is not recommended. In the ObservableCollection, Items is a protected property and is not suitable as a binding property.
You can try to bind dependency property directly in ItemsControl:
<ItemsControl ItemsSource="{x:Bind DropDownDataSource,Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="local:Item">
<CheckBox IsChecked="{x:Bind IsChecked, Mode=TwoWay}"
IsThreeState="False" >
<TextBlock Text="{x:Bind Text}" Foreground="Black" FontSize="24"/>
</CheckBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In addition, you may have noticed that I modified the style of CheckBox and rewritten the content to TextBlock, because in the default style of CheckBox, Foreground is not bound to the internal ContentPresenter.
Thanks.

How to create attached property for ItemsControl

I have the following ItemsControl, as shown it has hard-coded values, I would like to shift these values into an attached property, probably an ObservableCollection or something similar.
How to create this attached property and how to bind it.
<ItemsControl Grid.Column="0" VerticalAlignment="Stretch" Name="ItemsSelected">
<sys:Double>30</sys:Double>
<sys:Double>70</sys:Double>
<sys:Double>120</sys:Double>
<sys:Double>170</sys:Double>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Fill="SlateGray" Width="18" Height="4"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
[EDIT]
So I think I have the attached property figured:
public static class ScrollBarMarkers
{
public static readonly DependencyProperty MarkersSelectedCollectionProperty =
DependencyProperty.RegisterAttached("MarkersSelectedCollection", typeof(ObservableCollection<double>), typeof(ScrollBarMarkers), new PropertyMetadata(null));
public static ObservableCollection<double> GetMarkersSelectedCollection(DependencyObject obj)
{
return (ObservableCollection<double>)obj.GetValue(MarkersSelectedCollectionProperty);
}
public static void SetMarkersSelectedCollection(ItemsControl obj, ObservableCollection<double> value)
{
obj.SetValue(MarkersSelectedCollectionProperty, value);
}
}
What I'm wondering now is the best way to get the ItemsControl object before calling the following in the selection changed behavior:
ScrollBarMarkers.SetMarkersSelectedCollection(ItemsControl, initSelected);
The style of the customized vertical scrollbar is setup in the Window.Resources
The behavior is set up on the DataGrid like so:
<DataGrid Name="GenericDataGrid">
<i:Interaction.Behaviors>
<helpers:DataGridSelectionChanged />
</i:Interaction.Behaviors>
</DataGrid>
My selection changed behavior:
public class DataGridSelectionChanged : Behavior<DataGrid>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.SelectionChanged += DataGrid_SelectionChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.SelectionChanged -= DataGrid_SelectionChanged;
}
void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ObservableCollection<double> initSelected = new ObservableCollection<double>();
initSelected.Add(30);
initSelected.Add(60);
initSelected.Add(100);
//Just trying to figure out how best to get the ItemsControl object.
ScrollBarMarkers.SetMarkersSelectedCollection(itemsControlObj, initSelected);
}
}
Below is an example of the markers in the scrollbar, a ItemsControl has been added to the custom vertical scrollbar as per the code right at the top of the question.
If I understand your question, you want bind an ObservableCollection to ItemsControl and when the items are long the scrollbar will appear.
This solution could serve you.
[I will working with MVVM]
You can create a ObservableCollection in your code.
private ObservableCollection<int> _list = new ObservableCollection<int>();
public ObservableCollection<int> List
{
get { return _list ; }
set { _list = value; RaisePropertyChanged("List"); }
}
Now, you binding Collection to ItemsControl
<ScrollViewer HorizontalAlignment="Left" Width="254" Height="Auto" >
<ItemsControl x:Name="ItemsControlComputers" ItemsSource="{Binding List, Mode=TwoWay}" Height="Auto"
HorizontalAlignment="Left" Width="254" ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
Background="{x:Null}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Width="254">
<TextBlock Text="{Binding }" Margin="4,0,0,5" VerticalAlignment="Center">
</TextBlock>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
Went down the wrong track with this instead of creating a DependencyProperty I should have just created a plain property, however because it is UI related I did not want it with my ViewModel. So I created a class with singleton pattern in the same namespace as my behavior and other attached properties. This also means I can set the collection from any behaviors.
Here is the binding:
<ItemsControl ItemsSource="{Binding Source={x:Static helpers:MyClass.Instance}, Path=SelectedMarkers}">
Here is the class with singleton pattern
public class MyClass : INotifyPropertyChanged
{
public static ObservableCollection<double> m_selectedMarkers = new ObservableCollection<double>();
public ObservableCollection<double> SelectedMarkers
{
get
{
return m_selectedMarkers;
}
set
{
m_selectedMarkers = value;
NotifyPropertyChanged();
}
}
private static MyClass m_Instance;
public static MyClass Instance
{
get
{
if (m_Instance == null)
{
m_Instance = new MyClass();
}
return m_Instance;
}
}
private MyClass()
{
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}

Attached property attached only on the first user control instance

Following Josh Smith example on mvvm workspaces (customers view), I have a mainwindow and a mainwindowviewmodel which contains an ObservableCollection of "ChatTabViewModel":
internal class FriendsListViewModel : ObservableObject
{
#region bound properties
private ICollectionView viewfriends;
private ObservableCollection<ChatTabViewModel> _chatTab;
...
#endregion
}
I have an area dedicated to this collection in the xaml like that :
<ContentControl Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" Content="{Binding Path=ChatTabs}" ContentTemplate="{StaticResource ChatTabsTemplate}" />
And in my resources dictionary:
<DataTemplate DataType="{x:Type vm:ChatTabViewModel}">
<View:ChatTabView />
</DataTemplate>
<DataTemplate x:Key="ClosableTabItemTemplate">
<DockPanel>
<Button
Command="{Binding Path=CloseCommand}"
Content="X"
Cursor="Hand"
DockPanel.Dock="Right"
Focusable="False"
FontFamily="Courier"
FontSize="9"
FontWeight="Bold"
Margin="0,1,0,0"
Padding="0"
VerticalContentAlignment="Bottom"
Width="16" Height="16"
/>
<ContentPresenter
Content="{Binding Path=Caption, Mode=OneWay}"
VerticalAlignment="Center">
</ContentPresenter>
</DockPanel>
</DataTemplate>
<DataTemplate x:Key="ChatTabsTemplate">
<TabControl
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource ClosableTabItemTemplate}"
Margin="4"/>
</DataTemplate>
On user event I add a new ChattabViewModel in my collection and the view related to it appears in the main window.
But when I tried to add an attached property on a scrollbar in the ChattabView, this property will attach only on the first ChattabViewModel instance, the other tabs won't be bound to the attached property. Here's the ChattabView XAML:
<ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="0">
<ItemsControl ItemsSource="{Binding Messages}" View:ItemsControlBehavior.ScrollOnNewItem="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox IsReadOnly="True" TextWrapping="Wrap" Text="{Binding Path=DataContext, RelativeSource={RelativeSource Self}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
and the code of the attached property:
namespace GtalkOntre.View
{
/// <summary>
/// Util class to scroll down when a new message is added.
/// </summary>
/// <remarks>attached property called ScrollOnNewItem that when set to true hooks into the INotifyCollectionChanged events of the itemscontrol items source and upon detecting a new item, scrolls the scrollbar to it.</remarks>
public class ItemsControlBehavior
{
static Dictionary<ItemsControl, Capture> Associations = new Dictionary<ItemsControl, Capture>();
public static bool GetScrollOnNewItem(DependencyObject obj)
{
return (bool)obj.GetValue(ScrollOnNewItemProperty);
}
public static void SetScrollOnNewItem(DependencyObject obj, bool value)
{
obj.SetValue(ScrollOnNewItemProperty, value);
}
public static DependencyProperty ScrollOnNewItemProperty =
DependencyProperty .RegisterAttached(
"ScrollOnNewItem",
typeof(bool),
typeof(ItemsControlBehavior),
new UIPropertyMetadata(false, OnScrollOnNewItemChanged));
public static void OnScrollOnNewItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var mycontrol = d as ItemsControl;
if (mycontrol == null) return;
bool newValue = (bool)e.NewValue;
if (newValue)
{
mycontrol.Loaded += MyControl_Loaded;
mycontrol.Unloaded += MyControl_Unloaded;
}
else
{
mycontrol.Loaded -= MyControl_Loaded;
mycontrol.Unloaded -= MyControl_Unloaded;
if (Associations.ContainsKey(mycontrol))
Associations[mycontrol].Dispose();
}
}
static void MyControl_Unloaded(object sender, RoutedEventArgs e)
{
var mycontrol = (ItemsControl)sender;
Associations[mycontrol].Dispose();
mycontrol.Unloaded -= MyControl_Unloaded;
}
static void MyControl_Loaded(object sender, RoutedEventArgs e)
{
var mycontrol = (ItemsControl)sender;
var incc = mycontrol.Items as INotifyCollectionChanged;
if (incc == null) return;
mycontrol.Loaded -= MyControl_Loaded;
Associations[mycontrol] = new Capture(mycontrol);
}
class Capture : IDisposable
{
public ItemsControl mycontrol { get; set; }
public INotifyCollectionChanged incc { get; set; }
public Capture(ItemsControl mycontrol)
{
this.mycontrol = mycontrol;
incc = mycontrol.ItemsSource as INotifyCollectionChanged;
incc.CollectionChanged +=incc_CollectionChanged;
}
void incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
ScrollViewer sv = mycontrol.Parent as ScrollViewer;
sv.ScrollToBottom();
}
}
public void Dispose()
{
incc.CollectionChanged -= incc_CollectionChanged;
}
}
}
}
So why is the attached property only bound once, on the first "chattabview" occurence of the chattabviewmodel collection? and therefore, working only on the first chattabviewmodel.
When I close them all, the attached property will unbind itself on the last instance of chattabviewmodel, and when I add a new first chattabviewmodel, the property will bind correctly. So it triggers only on the first instance and last instance of the "chattabviewmodel" collection of mainwindowviewmodel.
After a week of searching, I'm a little desperate now...
So far my hypothesis is : the problem might be related to the way I set the view to my viewmodel in dictionary resources. The view might be shared and the first scrollbar only might react. I tried to add an x:Shared = false attribute on the DataTemplate tag but it didn't change anything.
Are you sure there are different instances of your ChatTabView being created?
I believe WPF's TabControl re-uses the existing template if it's the same instead of creating a new one, and simply replaces the DataContext behind it.
So it would only create one copy of your ChatTabView and switching tabs is replacing the DataContext behind the ChatTabView to a different item in the collection.
You haven't shown us ChatTabsTemplate, so I can only assume it contains a TabControl. If so, that explains the behavior you're seeing. The TabControl lazily loads its child tab items, so only the current view will be initialized, and hence have the attached property applied to it. When you switch tabs, however, you should see the same attached property firing. Is that not the case?
As for your hunch, it's not quite right. The DataTemplate is being shared, but the DataTemplate is used to create distinct instances of its contents, which are not being shared.

Silverlight - Can't get ListBox to bind to my ObservableCollection

I'm trying to make a ListBox that updates to the contents of an ObservableCollection whenever anything in that collection changes, so this is the code I've written for that:
xaml:
<ListBox x:Name="UserListTest" Height="300" Width="200" ItemsSource="listOfUsers">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding LastName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
C#:
public ObservableCollection<User> listOfUsers
{
get { return (ObservableCollection<User>)GetValue(listOfUsersProperty); }
set { SetValue(listOfUsersProperty, value); }
}
public static readonly DependencyProperty listOfUsersProperty =
DependencyProperty.Register("listOfUsers", typeof(ObservableCollection<User>), typeof(MainPage), null);
And I set up a call to a WCF Service that populates listOfUsers:
void repoService_FindAllUsersCompleted(object sender, FindAllUsersCompletedEventArgs e)
{
this.listOfUsers = new ObservableCollection<User>();
foreach (User u in e.Result)
{
listOfUsers.Add(u);
}
//Making sure it got populated
foreach (User u in listOfUsers)
{
MessageBox.Show(u.LastName);
}
}
The ListBox never populates with anything. I assume my problem may be with the xaml since the ObservableCollection actually has all of my Users in it.
You're missing the {Binding} part from your ItemsSource there.
<ListBox x:Name="UserListTest" Height="300" Width="200" ItemsSource="{Binding listOfUsers}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding LastName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Also, you may not need to have a DependencyProperty for your list, you may be able to achieve what you need with a property on a class that implements INotifyPropertyChanged. This may be a better option unless you need a DependencyProperty (and the overhead that goes along with it) for some other reason.
e.g.
public class MyViewModel : INotifyPropertyChanged
{
private ObservableCollection<User> _listOfUsers;
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<User> ListOfUsers
{
get { return _listOfUsers; }
set
{
if (_listOfUsers == value) return;
_listOfUsers = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("ListOfUsers"));
}
}
}

Categories

Resources