How to have a dynamic DataTemplateSelector - c#

I have an observable collection that I am displaying in a Xamarin Forms ListView. I have defined a detail and a summary template that I use to view each list item. I want to be able to dynamically change between summary and detail template based on a Boolean property in each item.
Here is the item.
public class MyItem : INotifyPropertyChanged
{
bool _switch = false;
public bool Switch
{
get
{
return _switch;
}
set
{
if (_switch != value)
{
_switch = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Switch"));
}
}
}
public int Addend1 { get; set; }
public int Addend2 { get; set; }
public int Result
{
get
{
return Addend1 + Addend2;
}
}
public string Summary
{
get
{
return Addend1 + " + " + Addend2 + " = " + Result;
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Here is the observable collection. Note that whenever the switch value changes I remove the item and reinsert. The reason this is done is to force the ListView to reselect the DataTemplate.
public class MyItems : ObservableCollection<MyItem>
{
protected override void InsertItem(int index, MyItem item)
{
item.PropertyChanged += MyItems_PropertyChanged;
base.InsertItem(index, item);
}
protected override void RemoveItem(int index)
{
this[index].PropertyChanged -= MyItems_PropertyChanged;
base.RemoveItem(index);
}
private void MyItems_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
int index = IndexOf(sender as MyItem);
if(index >= 0)
{
RemoveAt(index);
Insert(index, sender as MyItem);
}
}
}
Here is my data template selector...
public class MyItemTemplateSelector : DataTemplateSelector
{
DataTemplate Detail { get; set; }
DataTemplate Summary { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
if(item is MyItem)
{
return (item as MyItem).Switch ? Detail : Summary;
}
return null;
}
}
Here are my resource definitions...
<DataTemplate x:Key="MyDetail">
<ViewCell>
<StackLayout Orientation="Horizontal">
<Switch IsToggled="{Binding Switch}"/>
<Entry Text="{Binding Addend1}"/>
<Entry Text="{Binding Addend2}"/>
<Label Text="{Binding Result}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
<DataTemplate x:Key="MySummary">
<ViewCell>
<StackLayout Orientation="Horizontal">
<Switch IsToggled="{Binding Switch}"/>
<Label Text="{Binding Summary}" VerticalOptions="Center"/>
</StackLayout>
</ViewCell>
</DataTemplate>
<local:MyItemTemplateSelector x:Key="MySelector" Detail="{StaticResource MyDetail}" Summary="{StaticResource MySummary}"/>
Here is my collection initialization...
MyItems = new MyItems();
MyItems.Add(new MyItem() { Switch = true, Addend1 = 1, Addend2 = 2 });
MyItems.Add(new MyItem() { Switch = false, Addend1 = 1, Addend2 = 2 });
MyItems.Add(new MyItem() { Switch = true, Addend1 = 2, Addend2 = 3 });
MyItems.Add(new MyItem() { Switch = false, Addend1 = 2, Addend2 = 3 });
And this is what it looks like...
Right. So everything works fine. If the switch is toggled the view of the item changes from summary to detail. The problem is that this cannot be the right way of doing this! It is a complete kluge to remove a list item and put it back in the same place in order to get the data template to reselect. But I cannot figure out another way of doing it. In WPF I used a data trigger in an item container style to set the content template based on the switch value, but there seems to be no way to do the equivalent thing in Xamarin.

The way to do this is not through switching templates, but defining a content view as the template and changing the visibility of controls within the template. There is apparently no way to get the ListView to re-evaluate the item template on an item short of removing it and re-adding it.
Here is my content view...
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamarinFormsBench"
x:Class="XamarinFormsBench.SummaryDetailView">
<ContentView.Content>
<StackLayout x:Name="stackLayout" Orientation="Horizontal">
<Switch x:Name="toggle" IsToggled="{Binding Switch}"/>
<Entry x:Name="addend1" Text="{Binding Addend1}"/>
<Entry x:Name="addend2" Text="{Binding Addend2}"/>
<Label x:Name="result" Text="{Binding Result}"/>
<Label x:Name="summary" Text="{Binding Summary}" VerticalOptions="Center"/>
</StackLayout>
</ContentView.Content>
This is the code behind...
namespace XamarinFormsBench
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class SummaryDetailView : ContentView
{
public SummaryDetailView()
{
InitializeComponent();
toggle.PropertyChanged += Toggle_PropertyChanged;
UpdateVisibility();
}
private void Toggle_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if(e.PropertyName == "IsToggled")
{
UpdateVisibility();
}
}
private void UpdateVisibility()
{
bool isDetail = toggle.IsToggled;
addend1.IsVisible = isDetail;
addend2.IsVisible = isDetail;
result.IsVisible = isDetail;
summary.IsVisible = !isDetail;
InvalidateLayout(); // this is key!
}
}
}
Now the main page contains this...
<ListView ItemsSource="{Binding MyItems}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<local:SummaryDetailView/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The key to making this work properly is to invalidate the layout of the ContentView when switching between summary and detail. This forces the ListView to layout the cell again. Without this the controls that are made invisible disappear the controls made visible never show. You do not need this if the ContentView is used outside of the ListView. This seems to me to be a bug in the ListView. You could get the item template switching to work if you could invalidate the layout of the ViewCell, but there is no public method (only a protected one) to do this.

This was tricky issue for me few years ago. I've came to MarkupExtensions and converters (IValueConverter). After heavy struggle with XAML extensions realm I've figured an obvious thing: it shouldn't be done like that.
For dynamic change of (m)any property(ies) of the component you should use Styles. Reactions of property (it has to be DependencyProperty to work with components) changes are simple to set via Stryle.Triggers and Setters.
<Style x:Key="imbXmlTreeView_itemstyle" TargetType="TreeViewItem">
<Setter Property="Margin" Value="-23,0,0,0" />
<Setter Property="Padding" Value="1" />
<Setter Property="Panel.Margin" Value="0"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource fade_lightGray}" />
<Setter Property="Foreground" Value="{DynamicResource fade_darkGray}" />
</Trigger>
<Trigger Property="IsSelected" Value="False">
<Setter Property="Background" Value="{DynamicResource fade_lightGray}" />
<Setter Property="Foreground" Value="{DynamicResource fade_darkGray}" />
</Trigger>
</Style.Triggers>
</Style>
Consider above (just copied from my old project): DynamicResource can be your DataTemplate.
Here is more accurate example you might use:
<Style x:Key="executionFlowBorder" TargetType="ContentControl" >
<Setter Property="Margin" Value="5" />
<Setter Property="ContentTemplate" >
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Vertical">
<Border Style="{DynamicResource executionBorder}" DataContext="{Binding}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<CheckBox IsChecked="{Binding Path=isExecuting}" Content="" Grid.Column="0" VerticalAlignment="Center"/>
<Label Content="{Binding Path=displayName, Mode=OneWay}" FontSize="10" Grid.Column="1" FontStretch="Expanded" FontWeight="Black"/>
<Image Source="{Binding Path=iconSource, Mode=OneWay}" Width="16" Height="16" Grid.Column="2" HorizontalAlignment="Right" Margin="0,0,5,0"/>
</Grid>
</Border>
<Label Content="{Binding Path=displayComment, Mode=OneWay}" FontSize="9" HorizontalAlignment="Left"/>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
Where the value of setter can be DynamicResource or one delivered via your MarkupExtension - some thing like I had here:
using System;
using System.Windows;
using System.Windows.Markup;
#endregion
/// <summary>
/// Pristup glavnom registru resursa
/// </summary>
[MarkupExtensionReturnType(typeof (ResourceDictionary))]
public class masterResourceExtension : MarkupExtension
{
public masterResourceExtension()
{
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
try
{
return imbXamlResourceManager.current.masterResourceDictionary;
}
catch
{
return null;
}
}
}
The MarkupExtensions you are using as in example below:
In the XAML code:
<Image Grid.Row="1" Name="image_splash" Source="{imb:imbImageSource ImageName=splash}" Stretch="Fill" />
Added later: just don't forget to add namespace/assembly reference (pointing to the code with the custom MarkupExtension) at top of the XAML Window/Control (in this example it is imbCore.xaml from separate library project of the same solution):
<Window x:Class="imbAPI.imbDialogs.imbSplash"
xmlns:imb="clr-namespace:imbCore.xaml;assembly=imbCore"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{Binding Path=splashTitle}" Height="666" Width="896" ResizeMode="NoResize" WindowStyle="ToolWindow" Topmost="False" WindowStartupLocation="CenterScreen"
xmlns:imbControls="clr-namespace:imbAPI.imbControls">
<Grid>
Also have in mind you have to compile it first in order to get it working in XAML designer.
The C# code of the extension used:
using System;
using System.Windows.Markup;
using System.Windows.Media;
using imbCore.resources;
#endregion
[MarkupExtensionReturnType(typeof (ImageSource))]
public class imbImageSourceExtension : MarkupExtension
{
public imbImageSourceExtension()
{
}
public imbImageSourceExtension(String imageName)
{
this.ImageName = imageName;
}
[ConstructorArgument("imageName")]
public String ImageName { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
try
{
if (imbCoreApplicationSettings.doDisableIconWorks) return null;
return imbIconWorks.getIconSource(ImageName);
}
catch
{
return null;
}
}
}
Hope I got your question right on the first place :).
Now I have to sleap :). Good luck!
Added later: ok, I missed your point :) sorry. However, I would leave the response in case you find something useful in the codes I've posted. Bye!

Related

WPF - UserControl constructing performance (very poor)

I have a more complex code on my hand, but to ask this question I am bringing a simpler example of code.
My App is going to iterate throughout all glyphs in a specific font (expected 500 to 5000 glyphs). Each glyph should have a certain custom visual, and some functionality in it. For that I thought that best way to achieve that is to create a UserControl for each glyph.
On the checking I have made, as my UserControl gets more complicated, it takes more time to construct it. Even a simple adding of Style makes a meaningful effect on the performance.
What I have tried in this example is to show in a ListBox 2000 glyphs. To notice the performance difference I put 2 ListBoxes - First is binding to a simple ObservableCollection of string. Second is binding to ObservableCollection of my UserControl.
This is my MainWindow xaml:
<Grid Background="WhiteSmoke">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<ListBox Margin="10" ItemsSource="{Binding MyCollection}"></ListBox>
<ListBox Margin="10" Grid.Row="1" ItemsSource="{Binding UCCollection}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"></ListBox>
</Grid>
On code behind I have 2 ObservableCollection as mentioned:
public static ObservableCollection<string> MyCollection { get; set; } = new ObservableCollection<string>();
public static ObservableCollection<MyUserControl> UCCollection { get; set; } = new ObservableCollection<MyUserControl>();
For the first List of string I am adding like this:
for (int i = 0; i < 2000; i++)
{
string glyph = ((char)(i + 33)).ToString();
string hex = "U+" + i.ToString("X4");
MyCollection.Add($"Index {i}, Hex {hex}: {glyph}");
}
For the second List of MyUserControl I am adding like this:
for (int i = 0; i < 2000; i++)
{
UCCollection.Add(new MyUserControl(i + 33));
}
MyUserControl xaml looks like this:
<Border Background="Black" BorderBrush="Orange" BorderThickness="2" MinWidth="80" MinHeight="80">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="2*"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Center" Foreground="White" FontSize="40" Text="{Binding Glyph}"/>
<TextBlock HorizontalAlignment="Center" Foreground="OrangeRed" Text="{Binding Index}" Grid.Row="1"/>
<TextBlock HorizontalAlignment="Center" Foreground="White" Text="{Binding Hex}" Grid.Row="2"/>
</Grid>
</Border>
And code behind of MyUserControl:
public partial class MyUserControl : UserControl
{
private int OrgIndex { get; set; } = 0;
public string Hex => "U+" + OrgIndex.ToString("X4");
public string Index => OrgIndex.ToString();
public string Glyph => ((char)OrgIndex).ToString();
public MyUserControl(int index)
{
InitializeComponent();
OrgIndex = index;
}
}
In order to follow the performance issue I have used Stopwatch. Adding 2000 string items to the first list took 1ms. Adding 2000 UserControls to the second list took ~1100ms. And it is just a simple UserControl, when I add some stuff to it, it takes more time and performance getting poorer. For example if I just add this Style to Border time goes up to ~1900ms:
<Style TargetType="{x:Type Border}" x:Key="BorderMouseOver">
<Setter Property="Background" Value="Black" />
<Setter Property="BorderBrush" Value="Orange"/>
<Setter Property="MinWidth" Value="80"/>
<Setter Property="MinHeight" Value="80"/>
<Setter Property="BorderThickness" Value="2" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, RelativeSource={RelativeSource FindAncestor, AncestorType=UserControl}}" Value="True">
<Setter Property="Background" Value="#FF2A3137" />
<Setter Property="BorderBrush" Value="#FF739922"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
I am not fully familiar with WPF work around, so I will really appreciate your help. Is this a totally wrong way to do this? I have read some posts about it, but could not manage to go through this: here, and here, and here and here and more.
This example full project can be downloaded Here
For your case, you can create DependencyProperty in your user control like so (just an example).
#region DP
public int OrgIndex
{
get => (int)GetValue(OrgIndexProperty);
set => SetValue(OrgIndexProperty, value);
}
public static readonly DependencyProperty OrgIndexProperty = DependencyProperty.Register(
nameof(OrgIndex), typeof(int), typeof(MyUserControl));
#endregion
And other properties can be set as DP or handle in init or loaded event...
Then use your usercontrol in listbox as itemtemplate...
<ListBox
Grid.Row="1"
Margin="10"
ItemsSource="{Binding IntCollection}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ListBox.ItemTemplate>
<DataTemplate>
<local:MyUserControl OrgIndex="{Binding Path=.}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And in your vm, create simple type list
public static ObservableCollection<int> IntCollection { get; set; } = new ObservableCollection<int>();
for (int i = 0; i < rounds; i++)
{
IntCollection.Add(i + 33);
}
It's quite faster than create a usercontrol list, and you can have your usercontrol and its style as a listviewitem
What solve this issue for now, is following #Andy suggestion to use MVVM approach. It was a bit complicated for me, and had to do some learning around.
What I did:
Cancaled the UserControl.
Created a class GlyphModel. That represents each glyph and it's information.
Created a class GlyphViewModel. That builds an ObservableCollection list.
Set the design for the GlyphModel as a ListBox.ItemTemplate.
So now GlyphModel class, implants INotifyPropertyChanged and looks like this:
public GlyphModel(int index)
{
_OriginalIndex = index;
}
#region Private Members
private int _OriginalIndex;
#endregion Private Members
public int OriginalIndex
{
get { return _OriginalIndex; }
set
{
_OriginalIndex = value;
OnPropertyChanged("OriginalIndex");
}
}
public string Hex => "U+" + OriginalIndex.ToString("X4");
public string Index => OriginalIndex.ToString();
public string Glyph => ((char)OriginalIndex).ToString();
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion INotifyPropertyChanged Members
And GlyphViewModel class looks like this:
public static ObservableCollection<GlyphModel> GlyphModelCollection { get; set; } = new ObservableCollection<GlyphModel>();
public static ObservableCollection<string> StringCollection { get; set; } = new ObservableCollection<string>();
public GlyphViewModel(int rounds)
{
for (int i = 33; i < rounds; i++)
{
GlyphModel glyphModel = new GlyphModel(i);
GlyphModelCollection.Add(glyphModel);
StringCollection.Add($"Index {glyphModel.Index}, Hex {glyphModel.Hex}: {glyphModel.Glyph}");
}
}
In the MainWindow XML I have defined the list with DataTemplate:
<ListBox.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource BorderMouseOver}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2*"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Center" Foreground="White" FontSize="40" Text="{Binding Glyph}" />
<TextBlock HorizontalAlignment="Center" Foreground="OrangeRed" Text="{Binding Index}" Grid.Row="1" />
<TextBlock HorizontalAlignment="Center" Foreground="White" Text="{Binding Hex}" Grid.Row="2" />
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
And for last set the DataContext for the MainWindow:
DataContext = new GlyphViewModel(2000);
It does work, and works very fast even for 4000 glyphs. Hope this is the right way for doing that.

UserControl Listbox with a Generic ObservableCollection that can be Modified by Buttons?

I need to be able to display lists of data in a ListBox with buttons that can move the items Up and Down and Remove items from the ListBoxes and reflect that in the data models.
SampleDesign: http://bigriverrubber.com/_uploads/sites/2/usercontrollistbox.jpg
I plan on having multiple ListBoxes just like this with the same functionality across several windows, so I thought I could make a UserControl with the ListBox and buttons I need inside it and have the buttons modify the data. That way I could just pass an ObservableCollection to the UserControl and I wouldn't have to recreate the buttons each time.
What I found out, however, is that I can't move the items if they are bound to an ObservableCollection, which they need to be for my purposes. From what I've read, I need to modify the collection instead.
But how do I do that from the UserControl? If the Type of the ObservableCollection needs to be variable so the ListBox can display many Types of lists, how can I possibly hope to target it to gain access to the Move and Remove methods in the ObservableCollection class?
I've tried taking the ItemsSource which was set to the ObservableCollection and converting it into an ObservableCollection< dynamic > but that didn't work.
I've tried Casting it as an ObservableCollection< T > and ObservableCollection< object > among others to no avail.
I've even tried restructuring my ViewModels under a GenericViewModel with a property of ObservableCollection< dynamic >, which failed and left my code in ruin so I had to return to a backup.
I've used an ItemsControl that changes the ListBox depending on which DataType it finds, but that would still mean I have to make separate button events anyway, so what's the point?
I would post some code, but seeing how nothing I've done has worked in the slightest I doubt that it will help any. At this point I don't even know if what I'm intending can be done at all.
If there are any suggestions on what code to post, feel free to ask.
EDIT: Here is a GenericViewModel. It doesn't work because I don't know what to set "Anything" to. EDIT: Added the UserControl
public class GenericViewModel : Observable
{
//-Fields
private ObservableCollection<Anything> _items;
private Anything _selectedItem;
//-Properties
public ObservableCollection<Anything> Items
{
get { return _items; }
set { Set(ref _items, nameof(Items), value); }
}
public Anything SelectedItem
{
get { return _selectedItem; }
set { Set(ref _selectedItem, nameof(SelectedItem), value); }
}
//-Constructors
public GenericViewModel()
{
if (Items == null) Items = new ObservableCollection<Anything>();
}
//-Logic
public void MoveUp()
{
if (Items == null) return;
Helper.MoveItemUp(Items, _items.IndexOf(_selectedItem));
}
public void MoveDown()
{
if (Items == null) return;
Helper.MoveItemDown(Items, _items.IndexOf(_selectedItem));
}
public void Remove()
{
if (Items == null) return;
Helper.RemoveItem(Items, _items.IndexOf(_selectedItem));
}
}
UserControl
public partial class CustomListBox : UserControl
{
//-Fields
//-Properties
//-Dependencies
//-Constructor
public CustomListBox()
{
InitializeComponent();
}
//-Methods
private void ListboxButtonUp_Click(object sender, RoutedEventArgs e)
{
}
private void ListboxButtonDown_Click(object sender, RoutedEventArgs e)
{
}
private void ListboxButtonCopy_Click(object sender, RoutedEventArgs e)
{
}
private void ListboxButtonDelete_Click(object sender, RoutedEventArgs e)
{
}
private void BorderLayerThumbnail_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
}
private void BorderLayerThumbnail_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
}
}
<UserControl x:Class="BRRG_Scrubber.User_Controls.CustomListBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:BRRG_Scrubber"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="150">
<Grid Grid.Row="0" Margin="5,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding Name}" Grid.Row="0" FontSize="10" Foreground="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
<!--ItemsSource="{Binding Items}" SelectedItem="{Binding Current}"-->
<ListBox x:Name="listBoxPlus" Grid.Row="1" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" >
<ListBox.Resources>
<Style x:Key="{x:Type ScrollBar}" TargetType="{x:Type ScrollBar}">
<Setter Property="Stylus.IsFlicksEnabled" Value="True" />
<Style.Triggers>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="Width" Value="14" />
<Setter Property="MinWidth" Value="14" />
</Trigger>
</Style.Triggers>
</Style>
<DataTemplate DataType="{x:Type local:Document}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Variable}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Layer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.30*" />
<ColumnDefinition Width="0.70*" />
</Grid.ColumnDefinitions>
<Border x:Name="borderLayerThumbnail" BorderBrush="#FF707070" BorderThickness="1" Width="50" Height="50" MouseRightButtonDown="BorderLayerThumbnail_MouseRightButtonDown" MouseLeftButtonDown="BorderLayerThumbnail_MouseLeftButtonDown" >
<Border.Background>
<ImageBrush ImageSource="/BRRG_Scrubber;component/Resources/Images/checkerboardtile.jpg" ViewportUnits="Absolute" Stretch="None" Viewport="0,0,12,12" TileMode="Tile"/>
</Border.Background>
<Image Grid.Column="0" Source="{Binding Image}" Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Center" OpacityMask="Gray">
<Image.Style>
<Style TargetType="Image">
<Setter Property="Opacity" Value="1.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Visible}" Value="False">
<Setter Property="Opacity" Value="0.5"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="10,0,0,0">
<TextBox Text="{Binding Name}"/>
<TextBlock Text="{Binding Type, Mode=OneWay}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Text="👁" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Opacity" Value="1.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Visible}" Value="False">
<Setter Property="Opacity" Value="0.2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="🔒" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Opacity" Value="1.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Locked}" Value="False">
<Setter Property="Opacity" Value="0.2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.Resources>
</ListBox>
<WrapPanel Grid.Row="2" HorizontalAlignment="Right">
<WrapPanel.Resources>
<Style TargetType="Button">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
<Setter Property="Background" Value="{x:Null}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Width" Value="20" />
<Setter Property="Height" Value="20" />
</Style>
</WrapPanel.Resources>
<Button x:Name="listboxButtonUp" Content="▲" Click="ListboxButtonUp_Click"/>
<Button x:Name="listboxButtonDown" Content="▼" Click="ListboxButtonDown_Click"/>
<Button x:Name="listboxButtonCopy" Content="⧉" Click="ListboxButtonCopy_Click"/>
<Button x:Name="listboxButtonDelete" Content="⛞" Click="ListboxButtonDelete_Click"/>
</WrapPanel>
</Grid>
</UserControl>
I would really like to be able to create a modified ListBox in a UserControl with buttons that can move items Up and Down and Remove them from the list which I can use for any ObservableCollection of any unknown Type. The ListBoxes I need would all function exactly the same except their Type would be unknown until runtime.
EDIT: New Code From Ed's Suggestions
MainViewModel
public class MainViewModel : Observable
{
//-Fields
private Project _project;
private GenericViewModel<Document> _documentCollection;
private GenericViewModel<Variable> _variableCollection;
private GenericViewModel<Layer> _layerCollection;
//-Properties
public Project Project
{
get { return _project; }
set { Set(ref _project, nameof(Project), value); }
}
public GenericViewModel<Document> DocumentCollection
{
get { return _documentCollection; }
set { Set(ref _documentCollection, nameof(DocumentCollection), value); OnPropertyChanged(nameof(LayerCollection)); }
}
public GenericViewModel<Variable> VariableCollection
{
get { return _variableCollection; }
set { Set(ref _variableCollection, nameof(VariableCollection), value); }
}
public GenericViewModel<Layer> LayerCollection
{
get { return _layerCollection; }
set { Set(ref _layerCollection, nameof(LayerCollection), value); }
}
//-Constructors
public MainViewModel()
{
Project = new Project();
DocumentCollection = new GenericViewModel<Document>();
DocumentCollection.Items = Project.Documents;
}
//-Logic
}
Test Window with Bindings
<StackPanel>
<uc:CustomListBox DataContext="{Binding DocumentCollection}" Height="100"/>
<uc:CustomListBox DataContext="{Binding LayerCollection}" Height="200"/>
<ListBox ItemsSource="{Binding Project.Documents}" Height="100"/>
</StackPanel>
GenericViewModel
public class GenericViewModel<Anything> : Observable, ICollectionViewModel
{
//-Fields
private ObservableCollection<Anything> _items;
private Anything _selectedItem;
//-Properties
public ObservableCollection<Anything> Items
{
get { return _items; }
set { Set(ref _items, nameof(Items), value); }
}
public Anything SelectedItem
{
get { return _selectedItem; }
set { Set(ref _selectedItem, nameof(SelectedItem), value); }
}
//-Constructors
public GenericViewModel()
{
if (Items == null) Items = new ObservableCollection<Anything>();
}
//-Logic
...Removed For Brevity...
}
Document Model Class
public class Document : Anything
{
//-Fields
private string _filePath = "New Document";
private ObservableCollection<Layer> _layers;
private ObservableCollection<Selection> _selections;
//-Properties
public string FilePath
{
get { return _filePath; }
set { Set(ref _filePath, nameof(FilePath), value); }
}
public ObservableCollection<Layer> Layers
{
get { return _layers; }
set { Set(ref _layers, nameof(Layers), value); }
}
//-Constructors
public Document()
{
if (Layers == null) Layers = new ObservableCollection<Layer>();
if (Selections == null) Selections = new ObservableCollection<Selection>();
}
public Document(string filepath)
{
this.FilePath = filepath;
if (Layers == null) Layers = new ObservableCollection<Layer>();
if (Selections == null) Selections = new ObservableCollection<Selection>();
Layers.Add(new Layer("LayerOne "+Name));
Layers.Add(new Layer("LayerTwo " + Name));
Layers.Add(new Layer("LayerThree " + Name));
Selections.Add(new Selection());
Selections.Add(new Selection());
}
//-Gets
public string Name
{
get { return Path.GetFileNameWithoutExtension(FilePath); }
}
}
The big issue here seems to be that you can't cast to a generic class with an unknown type parameter, but you want your viewmodel class to be properly generic. That circle is squareable in two different useful ways, both valuable to know, so we'll do both.
The proper MVVM way to do this is to give your viewmodel some command properties which call these methods. A DelegateCommand class is the same as a RelayCommand class; the internet is full of implementations if you don't have one already.
public ICommand MoveUpCommand { get; } =
new DelegateCommand(() => MoveUp());
XAML:
<Button Content="▲" Command="{Binding MoveUpCommand}" />
Then get rid of those click event handlers. You don't need 'em. That very neatly solves your problem of calling those methods.
However, there's also a clean way to call those methods from code behind, and it's an important pattern to learn if you're going to be working with generics.
The classic solution to the casting problem is the one the framework uses for generic collections: Generic IEnumerable<T> implements non-generic System.Collections.IEnumerable. List<T> implements non-generic System.Collections.IList. Those non-generic interfaces provide the same operations, but in a non-generic way. You can always cast a List<T> to non-generic IList and call those methods and properties without knowing T.
Any well-designed collection can be assigned to a property of type IEnumerable: ListBox.ItemsSource is declared as System.Collections.IEnumerable, for example. Any collection can be assigned to it, without the ListBox needing to know what type is in the collection.
So let's write a non-generic interface that exposes the members we'll need to access without knowing any type parameters.
public interface ICollectionViewModel
{
void MoveUp();
void MoveDown();
void Remove();
}
If one of those method prototypes included the collection item type, say void RemoveItem(Anything x), that would complicate matters, but there's a classic solution to that problem as well.
Your Anything is already used like a type parameter. All we need to do is declare it as one. Your methods already have the appropriate prototypes to implement the interface methods.
public class GenericViewModel<Anything> : Observable, ICollectionViewModel
Instantiate like so:
this.DocumentCollection = new GenericViewModel<Document>();
Now your codebehind can cast any instance of GenericViewModel, regardless of type parameter, to a non-generic interface that supports the needed operations:
private void ListboxButtonUp_Click(object sender, RoutedEventArgs e)
{
if (DataContext is ICollectionViewModel icollvm)
{
icollvm.MoveUp();
}
}

WPF - Changes in treeviewitems' properties not reflected in UI problem

I have a tree view built with HierarchicalDataTemplates, I want to be able to add JSON files to SegmentInfo nodes - if I do so, the data is added but the change is not reflected in UI (still the comment says "no data" in red).
I've made the list the tree view items as ObservableCollection, moved it to a "ViewModel" class that inherits INotifyPropertyChanged, I seem to set it up properly, I've set DataContext to the ViewModel object in my Window.
In xaml I've set the bindings and mode as TwoWay. Still nothing helped
XAML:
<Window.Resources>
<local:BoolToStringConverter x:Key="BoolToStringConverter" FalseValue="no data" TrueValue="has data" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" MinHeight="384.8"/>
<RowDefinition Height="35.2"/>
</Grid.RowDefinitions>
<TreeView Name="trvTypeInfos" Margin="5" Grid.Row="0" ItemsSource="{Binding Path=TypeInfoList, Mode=TwoWay}">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="ListBoxItem.PreviewMouseUp"
Handler="ListBoxItem_PreviewMouseUp"/>
<Setter Property="IsExpanded" Value="True"/>
</Style>
<HierarchicalDataTemplate DataType="{x:Type data:TypeInfo}" ItemsSource="{Binding SegmentInfos, Mode=TwoWay}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Text=" [" Foreground="Blue" />
<TextBlock Text="{Binding SegmentInfos.Count}" Foreground="Blue"/>
<TextBlock Text="]" Foreground="Blue" />
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type data:SegmentInfo}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Text=" ("/>
<TextBlock Text="{Binding Path=HasData, Mode=TwoWay, Converter={StaticResource BoolToStringConverter}}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="Text" Value="no data">
<Setter Property="Foreground" Value="Red"/>
</Trigger>
<Trigger Property="Text" Value="has data">
<Setter Property="Foreground" Value="Green"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text=")"/>
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Width="80" Height="20" Content="OK" Margin="5,0, 5, 5" IsDefault="True" Click="OK_Click"/>
<Button Width="80" Height="20" Content="Cancel" Margin="5,0, 5, 5" Click="Cancel_Click" />
</StackPanel>
</Grid>
Window class:
public SegmentDataUpdaterDialog(SegmentDataUpdater segmentDataUpdater, List<TypeInfo> typeInfoList)
{
ViewModel = new ViewModel(typeInfoList);
DataContext = ViewModel;
SegmentDataUpdater = segmentDataUpdater;
InitializeComponent();
}
private void ListBoxItem_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
TreeViewItem item = sender as TreeViewItem;
SegmentInfo segInfo = item.Header as SegmentInfo;
if (segInfo != null)
{
MessageBox.Show(segInfo.JsonContents);
var filePath = AskForFile();
bool success = SegmentDataUpdater.TryStoreJson(segInfo, filePath, out string json);
if (success)
{
segInfo.JsonContents = json;
segInfo.HasData = true;
}
}
}
ViewModel class:
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<TypeInfo> _typeInfoList;
public ObservableCollection<TypeInfo> TypeInfoList
{
get { return _typeInfoList; }
set
{
if (_typeInfoList==null || !value.All(_typeInfoList.Contains))
{
_typeInfoList = value;
OnPropertyChanged(nameof(TypeInfoList));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public ViewModel(List<TypeInfo> typeInfos)
{
TypeInfoList = new ObservableCollection<TypeInfo>(typeInfos);
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
TypeInfo class:
public class TypeInfo
{
public string Name { get; set; }
public ObservableCollection<SegmentInfo> SegmentInfos { get; set; }
public int ElementId { get; set; }
public TypeInfo()
{
SegmentInfos = new ObservableCollection<SegmentInfo>();
}
}
SegmentInfo class:
public class SegmentInfo
{
public string Name { get; set; }
public bool HasData { get; set; }
public string JsonContents { get; set; }
public int ElementId { get; set; }
}
Converter classes:
public class BoolToValueConverter<T> : IValueConverter
{
public T FalseValue { get; set; }
public T TrueValue { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null)
return FalseValue;
else
return (bool)value ? TrueValue : FalseValue;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value != null ? value.Equals(TrueValue) : false;
}
}
public class BoolToStringConverter : BoolToValueConverter<String> { }
I expect that after successful adding json file to the SegmentInfo the UI will update the node with "has data" comment.
Now I can check that the data is really added to the SegmentInfo but UI doesn't reflect that.
Your HasData property does not update the UI, as you have no mechanism to update it (INotifyPropertyChanged). SegmentInfo needs to implement INotifyPropertyChanged.
If you plan to have a property Bind to the UI, it needs to have an PropertyChanged Notification go out for it individually. So on your SegmentInfo class; Name, HasData, and JsonContent should each raise an OnPropertyChanged event in their setter.
A good way to think of it; anything that is directly bound in XAML (Text="{Binding Name}") should raise an event when changed. If you bind any properties like: (Text="{Binding MyThing.Name}") you will not get an update when MyThing.Name changes. You need to break out the property and Notify on it directly.

Command Binding to UserControl

I am currently struggling to get something to work, which in my head does not seem to be that hard.
A Got a TopLevel User Control which is displayed in a Window:
<UserControl.Resources>
<DataTemplate DataType="{x:Type viewModels:PCodeViewModel}">
<controls:PCodeTabControl />
</DataTemplate>
<DataTemplate x:Key="TabItemHeaderTemplate">
<TextBlock FontWeight="Medium" Text="{Binding}" />
</DataTemplate>
<Style x:Key="TabItemStyle" TargetType="{x:Type dx:DXTabItem}">
<Setter Property="Header" Value="{Binding TabHeader}" />
<Setter Property="Content" Value="{Binding}" />
</Style>
<DataTemplate DataType="{x:Type viewModels:MexCompileViewModel}">
<controls:MexCompileTabControl />
</DataTemplate>
</UserControl.Resources>
<Grid>
<dx:DXTabControl ItemContainerStyle="{StaticResource TabItemStyle}"
ItemHeaderTemplate="{StaticResource TabItemHeaderTemplate}"
ItemsSource="{Binding Tabs}" />
</Grid>
The corresponding ViewModel is here:
private ICommand createNewProjectCommand;
private string sandboxRoot;
public MatlabBuildViewModel()
{
this.Init();
}
public void Init()
{
this.InitTabs();
}
public void InitTabs()
{
this.Tabs = new ObservableCollection<TabViewModelBase>
{
new MexCompileViewModel(),
new PCodeViewModel()
};
this.SandboxRoot = #"E:\_SupportTools\CaseManager";
}
public ObservableCollection<TabViewModelBase> Tabs { get; private set; }
public void NotifyChildren()
{
Messenger.Default.Send(new SandboxRootUpdated());
}
public string SandboxRoot
{
get
{
return this.sandboxRoot;
}
set
{
if (value != null)
{
this.sandboxRoot = value;
this.OnPropertyChanged();
this.NotifyChildren();
}
}
}
/// <summary>
/// Gets the create new project command.
/// </summary>
public ICommand CreateEmptyProjectCommand
{
get
{
if (this.createNewProjectCommand == null)
{
this.createNewProjectCommand = new DelegateCommand(Debugger.Break);
}
return this.createNewProjectCommand;
}
}
Now as you can see I am displaying two tabs by having a DataTemplate for the targetType MexCompileViewModel and PCodeViewModel.
Both userControls bound by a Dattemplate share a common UserControl which contains a number of buttons.
Here is the MexCompileTabControl as Example
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<compositeControls:MexCompileGrid Grid.Column="0" IsEnabled="{Binding IsEnabled}" />
<StackPanel Grid.Column="1">
<compositeControls:CommonActionsControl />
</StackPanel>
</Grid>
The CommonActionsControl is just a StackPanel with Buttons:
<StackPanel helpers:MarginSetter.Margin="3">
<GroupBox Header="MatlabProject-File">
<StackPanel helpers:MarginSetter.Margin="3">
<Button Command="{Binding CreateEmptyProjectCommand}" Content="Create empty project-file" />
<Button Content="Refresh" />
</StackPanel>
</GroupBox>
<GroupBox Header="Actions">
<StackPanel helpers:MarginSetter.Margin="3">
<Button Content="Clean" />
<Button Content="Rebuild" />
<Button Content="Generate m-Script" />
</StackPanel>
</GroupBox>
</StackPanel>
Code Behind:
public CommonActionsControl()
{
this.InitializeComponent();
}
public static readonly DependencyProperty CreateEmptyProjectCommandProperty = DependencyProperty.Register("CreateEmptyProjectCommand", typeof(ICommand), typeof(CommonActionsControl), new PropertyMetadata(default(ICommand)));
public ICommand CreateEmptyProjectCommand
{
get
{
return (ICommand)GetValue(CreateEmptyProjectCommandProperty);
}
set
{
this.SetValue(CreateEmptyProjectCommandProperty, value);
}
}
So what I am trying to achieve is:
My Commands are defined in the TopLevelViewModel. Now I want my CommonActionsControl inherit these Commands since the Control should be used multiple times. Can you help me with this?
Since CommonActionsControl displays common actions defined in the TopLevelViewModel, it would make sense to have them as part of the TopLevelView instead of displaying them on every tab.
If you really do want them on every tab, then I say your best bet is to add the commands to your TabViewModelBase so that the various tab views can bind to them. You're still free to implement them once in TopLevelViewModel, and just pass them into the various tab VMs by injecting them in the constructor.

Listbox IsSelected with SelectionMode=Extended

Sorry for the vague title, I couldn't come up with a good way to summarize what is happening.
I have a bound WPF listbox:
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:MyBoundObject}">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</UserControl.Resources>
<ListBox ItemsSource="{Binding SomeSource}" SelectionMode="Extended">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
I want to operate on ONLY the selected items. I do this by iterating through a list of all items and checking each object to see if it's IsSelected property is set.
This works except for when I have many items in the list (enough so they are not all visible) and I press CTRL-A to select all items. When I do this, all the visible items have their IsSelected property set to true, and all the rest are left false. As soon as I scroll down, the other items come into view and their IsSelected properties are then set to true.
Is there any way to fix this behaviour so that every object's IsSelected property is set to true when I press CTRL-A?
Try set the
ScrollViewer.CanContentScroll="False"
on the ListBox, it should fix the ctrl+a problem.
If you want get all selected items you can use SelectedItems property from ListBox. You don't need to add IsSelected property to your object.
Check below example.
XAML file:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Button Content="Selected items" Click="Button_Click" />
<Button Content="Num of IsSelected" Click="Button_Click_1" />
</StackPanel>
<ListBox Name="lbData" SelectionMode="Extended" Grid.Row="1">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
Code-behind file:
using System.Collections.Generic;
using System.Windows;
using System.Windows.Documents;
namespace ListBoxItems
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<MyBoundObject> _source = new List<MyBoundObject>();
for (int i = 0; i < 100000; i++)
{
_source.Add(new MyBoundObject { Label = "label " + i });
}
lbData.ItemsSource = _source;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(lbData.SelectedItems.Count.ToString());
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
int num = 0;
foreach (MyBoundObject item in lbData.Items)
{
if (item.IsSelected) num++;
}
MessageBox.Show(num.ToString());
}
}
public class MyBoundObject
{
public string Label { get; set; }
public bool IsSelected { get; set; }
}
}

Categories

Resources