Perhaps I'm going about this the wrong way, but my layout is in a way where I have multiple Expanders in a TabControl and I want to add an "expand all" button.
Now logically this button should be inside the tab as it would control the elements in the tab so they ought to be grouped together. Visually however this would be a waste of space as I got a lot of empty space on the Tab Header bar (not sure what the terminology is, the row with the tabheaders).
So what I'm trying to achieve is adding a button outside the content of the tab. The canvas element seems to be what I need to use and it's working as far as its repositioning the element but it gets cut off. This is much easier to explain with a picture so
(if you look hard you can see where the button is as the header covering it is slightly translucent)
Now I can position it where I'd like it to be by moving it outside the TabItem but then I would have to write code to see which tab is focussed and hide it when it's not "Current" that is focussed. That to me sounds like the wrong way to do it as the only thing I want to do is move a button which is a 'view' type of thing.
My MainWindow.axaml:
<TabControl Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">
<TabItem Header="Current" ZIndex="1">
<ScrollViewer Classes="CrawlersInAction">
<StackPanel>
<Canvas>
<Button Canvas.Right="10" Canvas.Top="-20" ZIndex="5">Expand All</Button>
</Canvas>
<!-- My very long template code for rendering the expanders -->
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
I do have a background in HTML/CSS so I thought Zindex would the trick and tried applying it in various places without any luck.
PS: I'm using Avalonia instead of WPF but it's pretty much a cross-platform clone, so any WPF know-how probably carries over 1:1.
If you think about it, this functionality lives in the ViewModel at the same "level" as the Tab Control.
<Grid>
<TabControl Items="{Binding MyTabViewModels}" SelectedItem={Binding SelectedTab} />
</Grid>
An Instance of MyTabViewModel has a collection on it:
public ObservableCollection<MyCollectionType> Items
The item class MyCollectionType has an IsExpanded property ...
public bool IsExpanded {get;set;}
Bound to your Expander control IsExpanded property.
Shove your button into the XAML
<Grid>
<TabControl Items="{Binding MyTabViewModels}" />
<Button Commmand={Binding ExpandAllCommand} />
</Grid>
Now on your base ViewModel your ICommand can do something like:
public void ExpandAllCommandExecuted()
{
foreach(var vm in SelectedTab.Items)
{
vm.IsExpanded = true;
}
}
Good luck, this is all pseudocode but illustrates a potential pattern.
The problem seems to have originated from placing my <canvas> control inside the <scrollviewer> control. I've placed it outside it whilst trying many things it seems and it works as I wanted it to. The buttons are visible rendering on top of the tabbar (TabStrip).
My XAML is now:
<TabControl Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">
<TabItem Header="Current">
<StackPanel>
<Canvas>
<StackPanel Orientation="Horizontal" Canvas.Right="0" Canvas.Bottom="10" Spacing="5">
<Button Command="{Binding CollapseAll}" IsEnabled="{Binding !AllAreCollapsed}">Collapse All</Button>
<Button Command="{Binding ExpandAll}" IsEnabled="{Binding !AllAreExpanded}">Expand All</Button>
</StackPanel>
</Canvas>
<ScrollViewer Classes="CrawlersInAction">
<StackPanel>
<ItemsControl Name="itemscontrol" Items="{Binding SiteInfos}" VerticalAlignment="Stretch">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander ExpandDirection="Down" IsExpanded="{Binding IsExpanded, Mode=TwoWay}" VerticalAlignment="Stretch">
<!-- Ommited my very long template code -->
</Expander>
<DataTemplate>
<ItemsControl.ItemTemplate>
<ItemsControl>
</StackPanel>
</ScrollViewer>
</StackPanel>
</TabItem>
</TabControl>
Codewise I ended up adding a "IsExpanded" property to my SiteInfo class that is used as the base for the expanders IsExpanded property and kept in sync by making it a two way binding as per the XAML above. The code on SiteInfo is:
public class SiteInfo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public static readonly bool StartIsExpanded = true;
private bool _isExpanded = StartIsExpanded;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != IsExpanded)
{
_isExpanded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsExpanded)));
}
}
}
When I create my SiteInfo objects in MainWindowViewModel I subscribe to the events (siteInfo.PropertyChanged += SiteInfo_PropertyChanged;). When the event is received and it would change if my collapse or expand all button should be disabled it sends it own PropertyChangedEvent which then enables/disabled the control.
public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
{
public new event PropertyChangedEventHandler? PropertyChanged;
public ObservableCollection<SiteInfo> SiteInfos { get; }
= new ObservableCollection<SiteInfo>();
//Change SiteInfo.StartExpanded if you want this changed.
private bool _allAreExpanded = SiteInfo.StartIsExpanded;
public bool AllAreExpanded
{
get => _allAreExpanded;
set
{
if (_allAreExpanded != value)
{
_allAreExpanded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllAreExpanded)));
}
}
}
//Change SiteInfo.StartExpanded if you want this changed.
private bool _allAreCollapsed = !SiteInfo.StartIsExpanded;
public bool AllAreCollapsed {
get { return _allAreCollapsed; }
set {
if (_allAreCollapsed != value)
{
_allAreCollapsed = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllAreCollapsed)));
}
}
}
private void SiteInfo_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if(e.PropertyName == nameof(siteInfo.IsExpanded))
{
AllAreCollapsed = AreAllCollapsed();
AllAreExpanded = AreAllExpanded();
}
}
public bool AreAllCollapsed()
{
return !SiteInfos.Any<SiteInfo>( siteInfo => siteInfo.IsExpanded );
}
public bool AreAllExpanded()
{
return !SiteInfos.Any<SiteInfo>( siteInfo => siteInfo.IsCollapsed);
}
public void CollapseAll()
{
foreach(SiteInfo siteInfo in SiteInfos)
{
siteInfo.IsExpanded = false;
}
}
public void ExpandAll()
{
foreach (SiteInfo siteInfo in SiteInfos)
{
siteInfo.IsExpanded = true;
}
}
}
Figured I'd add the rest of my code in case anyone Googles this up and wants to do something similar.
So now when my program loads and everything is set to the default expanded true Expand All is disabled, Collapse all is enabled. Changing one expander to collapsed status will have both buttons enabled and collapsing all expanders will disable the Collapse All button.
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
In WPF you can modify controls using Styles and Templates dynamically with Binding. I see how to do this in UWP directly in the control but I want to apply a template that will change itself based on the binding.
An example would be a button. I have a button that turns on and off a light in this project. The project is already created and running in WPF but needs to be converted to UWP. In the WPF version we have a LightStyle for the button, depending on what type of light it is, we change the Template to look and perform for that light. (For example: we can change the color of some lights, the dimness for some lights, and some lights just turn on and off; but we use the same LightStyle for them all. Very generic, dynamic, and extremely useful.)
How do you do this in UWP? I've searched a minute and figured I would stop here and check while I continue to dig. Keep in mind that this project is pure MVVM and no code behind is used. I don't mind a code behind explanation as long as it's not the only way.
Thanks in advance :)
Here is a sample I've made - XAML:
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Orientation="Horizontal">
<StackPanel.Resources>
<local:MySelector x:Key="MySelector">
<local:MySelector.GreenTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" Foreground="Green"/>
</DataTemplate>
</local:MySelector.GreenTemplate>
<local:MySelector.RedTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" Foreground="Red"/>
</DataTemplate>
</local:MySelector.RedTemplate>
</local:MySelector>
</StackPanel.Resources>
<ListView x:Name="ListOfItems" Width="100" ItemTemplateSelector="{StaticResource MySelector}"/>
<StackPanel>
<ToggleSwitch OnContent="GREEN" OffContent="RED" Margin="10" IsOn="{x:Bind IsSwitched, Mode=TwoWay}"/>
<Button Content="Add item" Click="AddClick" Margin="10"/>
</StackPanel>
</StackPanel>
and the code behind:
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaiseProperty(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
private bool isSwitched = false;
public bool IsSwitched
{
get { return isSwitched; }
set { isSwitched = value; RaiseProperty(nameof(IsSwitched)); }
}
public MainPage() { this.InitializeComponent(); }
private void AddClick(object sender, RoutedEventArgs e)
{
ListOfItems.Items.Add(new ItemClass { Type = isSwitched ? ItemType.Greed : ItemType.Red, Text = "NEW ITEM" });
}
}
public enum ItemType { Red, Greed };
public class ItemClass
{
public ItemType Type { get; set; }
public string Text { get; set; }
}
public class MySelector : DataTemplateSelector
{
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
switch ((item as ItemClass).Type)
{
case ItemType.Greed:
return GreenTemplate;
case ItemType.Red:
default:
return RedTemplate;
}
}
public DataTemplate GreenTemplate { get; set; }
public DataTemplate RedTemplate { get; set; }
}
Generally you can choose various switches for your selector, it depends on your needs. In above example I'm switching the template basing on the item's property, here is a good example how to switch on item's type.
Here's the answer that I am using that works for my given situation. Basically you have to use a VisualStateTrigger and create the trigger manually via code. There are various triggers you can use and many built in but for this situation I had to, or at least I think I had to, write one manually.
Here's the trigger code.
public class StringComparisonTrigger : StateTriggerBase
{
private const string NotEqual = "NotEqual";
private const string Equal = "Equal";
public string DataValue
{
get { return (string)GetValue(DataValueProperty); }
set { SetValue(DataValueProperty, value); }
}
public static readonly DependencyProperty DataValueProperty =
DependencyProperty.Register(nameof(DataValue), typeof(string), typeof(StringComparisonTrigger), new PropertyMetadata(Equal, (s, e) =>
{
var stringComparisonTrigger = s as StringComparisonTrigger;
TriggerStateCheck(stringComparisonTrigger, stringComparisonTrigger.TriggerValue, (string)e.NewValue);
}));
public string TriggerValue
{
get { return (string)GetValue(TriggerValueProperty); }
set { SetValue(TriggerValueProperty, value); }
}
public static readonly DependencyProperty TriggerValueProperty =
DependencyProperty.Register(nameof(TriggerValue), typeof(string), typeof(StringComparisonTrigger), new PropertyMetadata(NotEqual, (s, e) =>
{
var stringComparisonTrigger = s as StringComparisonTrigger;
TriggerStateCheck(stringComparisonTrigger, stringComparisonTrigger.DataValue, (string)e.NewValue);
}));
private static void TriggerStateCheck(StringComparisonTrigger elementTypeTrigger, string dataValue, string triggerValue)
=> elementTypeTrigger.SetActive(dataValue == triggerValue);
}
This, since inheriting from StateTriggerBase can be used in the VisualStateTriggers group as I will post below. What I didn't know is that any dependency property you write can be used in the XAML and there's no interfaces or anything in the trigger to make it work. The only line of code that fires the trigger is 'SetActive(bool value)' that you must call whenever you want the state to change. By making dependency properties and binding in the XAML you can fire the SetActive whenever the property is changed and therefore modify the visual state.
The DataTemplate is below.
<DataTemplate x:Key="LightsButtonTemplate">
<UserControl>
<StackPanel Name="panel">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState>
<VisualState.StateTriggers>
<DataTriggers:StringComparisonTrigger DataValue="{Binding Type}"
TriggerValue="READ" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="panel.(UIElement.Background)"
Value="Red" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<TextBlock Text="{Binding Type}" />
<TextBlock Text="{Binding LightStateViewModel.On}" />
</StackPanel>
</UserControl>
</DataTemplate>
And finally using you can use the DataTemplate anywhere but I am using it in an ItemsControl that is bound to a list of LightViewModels.
<ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding LightViewModels}"
ItemTemplate="{StaticResource LightsButtonTemplate}" />
</ScrollViewer>
Obviously this isn't the template design I want for the light buttons but this is all I've done to understand and now implement dynamic templates. Hopefully this helps someone else coming from WPF.
The custom trigger class deriving from StateTriggerBase can do and bind anyway you want it to and all you need to do is call SetActive(true) or SetActive(false) whenever you wish to update that trigger. When it's true the VisualState using that trigger will be active.
I have seen some answers regarding WP8 or others, however it seems that there is no triggers in WP8.1 (Or I am missing something?)
I have a datatemplate bound from the code (it is a hub datatemplate, and I have a mix of static and dynamic hubsections, therefore this datatemplate needs to be set from the code).
This datatemplate is defined in a separate xaml file, it includes a listbox (or listview) with another datatemplate defined for the items.
I need to bind a command on the item's tap or listbox selectionchanged (or something equivalent). However, the tap event defined in the template is not called, therefore I thought of binding a command on an UI element, but these seems not to support Commands neither interactivity triggers.
Any clue on how to handle that? :)
On the example below I don't get the event Item_Tapped nor ListBox_SelectionChanged, I would anyway prefer to bind one of these to a command in the viewmodel.
<DataTemplate x:Key="HubSectionTemplate">
<Grid>
<ListBox ItemsSource="{Binding MyNodes}"
SelectionChanged="ListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Height="64" Tapped="Item_Tapped" >
<TextBlock Text="{Binding MyText}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</DataTemplate>
This is how it is used from code:
HubSection hs = new HubSection()
{
ContentTemplate = Application.Current.Resources[HUBSECTION_TEMPLATE] as DataTemplate,
DataContext = model,
Tag = model.UniqueId,
};
Hub.Sections.Insert(firstSectIdx + 1, hs);
public class Model
{
public Guid UniqueId {get;set;}
public List<ItemModel> MyNodes {get;set;}
}
public class ItemModel
{
public string MyText {get;set;}
}
PS: The ItemModel is defined in another assembly and therefore should not be edited (the command should be in the Model class if possible)
--- EDIT ---
In order to simplify the problem, I use the following models:
public class Model
{
public Guid UniqueId {get;set;}
public List<ItemModel> MyNodes {get;set;}
public ICommand MyCommand {get;set;}
}
public class ItemModel
{
Model _Model;
public ItemModel(Model m) {_Model = m; }
public string MyText {get;set;}
public ICommand MyCommand { get { return _Model.MyCommand; }}
}
And my (temporary) solution is to use a button in the itemtemplate:
<ListView.ItemTemplate>
<DataTemplate>
<Button HorizontalAlignment="Stretch" Command="{Binding TapCommand}" Height="64">
<TextBlock Text="{Binding MyText}" />
</Button>
</DataTemplate>
</ListView.ItemTemplate>
You can use Behaviors SDK.
In Visual Studio go to 'Tools -> Extension and updates' and install Behaviors SDK (XAML). Then reference it in your project using Add reference dialog.
After that add following namespaces to your page:
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
Now you can register events like tap on your stack panel using following syntax:
<DataTemplate>
<StackPanel Orientation="Horizontal" Height="64">
<TextBlock Text="{Binding MyText}" />
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Tapped">
<core:InvokeCommandAction Command="{Binding YourCommand}"/>
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</StackPanel>
</DataTemplate>
However this code only works if your Command is defined in your ItemModel class. If you want to bind to the parent element Command, you can try something like this (not tested):
{Binding ElementName=LayoutRoot, Path=DataContext.ParentCommand}
But I would preferer having command on your ItemModel class
Edit: Solution without Behaviors SDK:
If you are using ListView (or something inherited from ListViewBase) you can use ItemClick event. To make it more reusable and Mvvm friendly you can implement your DependencyProperty like this:
public static class ItemClickCommand
{
public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command", typeof(ICommand),
typeof(ItemClickCommand), new PropertyMetadata(null, OnCommandPropertyChanged));
public static void SetCommand(DependencyObject d, ICommand value)
{
d.SetValue(CommandProperty, value);
}
public static ICommand GetCommand(DependencyObject d)
{
return (ICommand)d.GetValue(CommandProperty);
}
private static void OnCommandPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var control = d as ListViewBase;
if (control != null)
{
control.ItemClick += OnItemClick;
}
}
private static void OnItemClick(object sender, ItemClickEventArgs e)
{
var control = sender as ListViewBase;
var command = GetCommand(control);
if (command != null && command.CanExecute(e.ClickedItem))
{
command.Execute(e.ClickedItem);
}
}
}
Then your ListView will look like this:
<ListView
IsItemClickEnabled="True"
helpers:ItemClickCommand.Command="{Binding YourCommand}"
ItemsSource="{Binding MyNodes}"
ItemTemplate="{StaticResource YourDataTemplate}" />
In this case your child item is passed to your command as a parameter, so it should also solve your problem with your Command defined in parent model.
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.