WPF DropDownButton IsOpen binding breaks after opening drop down - c#

I have come across a peculiar issue I have had trouble fixing. Below is simplified code that shows the problem.
I have a WPF DropDownButton that contains a list of strings in it's drop down. I also have a search box. The idea being as you type in the search box, the drop down automatically expands and shows only the matching items (thus making it easier for you to find the one you are interested in). If there are no matching items or the search field is empty the drop down is closed. If there is a search term but no matching items the search field changes colour.
All this works fine. Until that is, you either clear the search field (when there was something to clear) using it's button or drop down the list using it's drop down arrow button. Once you do either of these the drop down no longer automatically opens and closes as you change the search term.
Once it is 'broken' the searching still works in that you can search for something e.g. 'Text 1' and if you open the drop down list it contains only the matching items. It is just the automatic opening and closing that no longer works.
I have checked and the ViewModel is raising the correct events. I have looked in to the DropDownButton code and can see it when it works the OnIsOpenChanged code is reached, but once broken it doesn't.
Could it be that by 'manually' opening the drop down I break/overwrite the IsOpen binding? If so how can I work round this. And why does clearing the search field break the binding as well? Removing the manual dropdown aspect is not an option.
EDIT
In the button click code behind (which I have tried replacing with a command but it make no difference to the behaviour) after setting the SeachExpression I added:
BindingExpression be = BindingOperations.GetBindingExpression(SelectButton, Xceed.Wpf.Toolkit.DropDownButton.IsOpenProperty);
which I think is the correct way to check if there is a binding and it comes back null. If I do this before setting the SeachExpression it is non-null.
ViewModel
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private IList<string> originalText;
public string SearchExpression
{
get
{
return _searchExpression;
}
set
{
if (value == _searchExpression)
return;
_searchExpression = value;
UpdateDescriptions(_searchExpression);
OnPropertyChanged("SearchExpression");
}
}
string _searchExpression = string.Empty;
public ReadOnlyObservableCollection<string> Descriptions
{
get { return _descriptions; }
private set
{
if (value == _descriptions)
return;
_descriptions = value;
OnPropertyChanged("Descriptions");
}
}
ReadOnlyObservableCollection<string> _descriptions;
public bool NoMatches
{
get { return _noMatches; }
private set
{
if (value == _noMatches)
return;
_noMatches = value;
OnPropertyChanged("NoMatches");
}
}
bool _noMatches;
public bool ShowSearchResults
{
get { return _showSearchResults; }
private set
{
if (value == _showSearchResults)
return;
_showSearchResults = value;
OnPropertyChanged("ShowSearchResults");
}
}
bool _showSearchResults;
public ViewModel()
{
originalText = new List<string>() {"Text 1", "Text 2", "Text 3"};
UpdateDescriptions();
}
private void UpdateDescriptions(string searchExpression = null)
{
ObservableCollection<string> descriptions = new ObservableCollection<string>();
IEnumerable<string> records;
if (string.IsNullOrWhiteSpace(searchExpression))
{
records = originalText;
NoMatches = false;
ShowSearchResults = false;
}
else
{
records = originalText.Where(x => x.Contains(searchExpression));
NoMatches = records.Count() < 1;
ShowSearchResults = records.Count() > 0;
}
descriptions = new ObservableCollection<string>(records);
this.Descriptions = new ReadOnlyObservableCollection<string>(descriptions);
}
protected virtual void OnPropertyChanged(string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
View
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpf="clr-namespace:Xceed.Wpf.Toolkit;assembly=WPFToolkit.Extended"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="65" Width="225">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<wpf:DropDownButton Grid.Column="0" x:Name="SelectButton" Content="Select" Margin="3 0" IsOpen="{Binding ShowSearchResults, UpdateSourceTrigger=PropertyChanged}">
<wpf:DropDownButton.DropDownContent>
<ListBox x:Name="descriptionsList" MaxHeight="250" ItemsSource="{Binding Path=Descriptions, Mode=OneWay}" HorizontalContentAlignment="Stretch">
<!-- this code makes sure the ListBox displays at the correct with for the outset, and does not resize as you scroll-->
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="Control.IsMouseOver" Value="True">
<Setter Property="Control.Background" Value="{x:Static SystemColors.HighlightBrush}" />
<Setter Property="Control.Foreground" Value="{x:Static SystemColors.HighlightTextBrush}" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</wpf:DropDownButton.DropDownContent>
</wpf:DropDownButton>
<Border Grid.Column="2" Background="White" BorderBrush="Gray" BorderThickness="1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<xctk:WatermarkTextBox x:Name="searchTextBox" Text="{Binding SearchExpression, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Watermark="Search" ToolTip="Search for an item in the list." >
<xctk:WatermarkTextBox.Style>
<Style>
<Setter Property="Control.BorderThickness" Value="0" />
<Style.Triggers>
<DataTrigger Binding="{Binding NoMatches}" Value="True" >
<Setter Property="TextBox.Background" Value="Tomato"/>
<Setter Property="TextBox.Foreground" Value="White"/>
</DataTrigger>
</Style.Triggers>
</Style>
</xctk:WatermarkTextBox.Style>
</xctk:WatermarkTextBox>
<Button Grid.Column="1"
x:Name="ClearButton"
ToolTip="Clear search text"
Click="ClearButton_Click"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Image Source="/WPFToolkit.Extended;component/PropertyGrid/Images/ClearFilter16.png" Width="16" Height="16" />
</Button>
</Grid>
</Border>
</Grid>
View Code Behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
private void ClearButton_Click(object sender, RoutedEventArgs e)
{
ViewModel vm = this.DataContext as ViewModel;
if (vm != null)
vm.SearchExpression = "";
}
}

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.

How to write the seats for a booking application in WPF?

I'm using right now this:
<Window.Resources>
<DataTemplate x:Key="DataTemplate_Level2">
<Button Content="{Binding}" Height="40" Width="50" Margin="4,4,4,4"/>
</DataTemplate>
<DataTemplate x:Key="DataTemplate_Level1">
<ItemsControl ItemsSource="{Binding}" ItemTemplate="{DynamicResource DataTemplate_Level2}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</Window.Resources>
With this:
<ItemsControl
x:Name="lst"
ItemTemplate="{DynamicResource DataTemplate_Level1}"
ItemsSource="{Binding TopSeats}"/>
I'm binding a List< List< int?>> as itemssource with numbers 0-2. 0 for empty, 1 for selected, 2 for booked.
List<List<int?>> topSeats;
public List<List<int?>> TopSeats
{
get => topSeats;
set
{
topSeats = value;
NotifyPropertyChanged("TopSeats");
}
}
My UI looks like this right now:
enter image description here
When i press a button, it should change from 0 to 1, and the corresponding element in the List< List< int?>> container should change too.
But i've arrived to a brick wall. I have no idea how to make sure that, when i press any button, the correct element changes in the "List< List< int?>>" container.
Is it possible somehow without code behind?
Its a big process to explain everything here. But I'll try my best to give you a working solution and hope you can read more about INotifyPropertyChanged, MVVM pattern and ICommand pattern.
For simplicity, I have not implemented ICommand here and using a code-behind click to get the selected seat numbers (This is only for testing to see if selected seat numbers are able to retrieve or not).
Step 1: I have created a Model class called Seat with following properties and I am implementing INotifyPropertyChanged interface to capture property changed events. See below for my Seat.cs class.
public class Seat : INotifyPropertyChanged
{
private int seatNo;
private string seatNumber;
private bool isSelected;
public int SeatNo
{
get { return seatNo; }
set { seatNo = value; OnPropertyChanged(); }
}
public string SeatNumber
{
get { return seatNumber; }
set { seatNumber = value; OnPropertyChanged(); }
}
public bool IsSelected
{
get { return isSelected; }
set { isSelected = value; OnPropertyChanged(); }
}
public void OnPropertyChanged([CallerMemberName]string popertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(popertyName));
}
private void BaseVM_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
}
public event PropertyChangedEventHandler PropertyChanged;
}
Step 2:
I have modified "DataTemplate_Level2" template to have a CheckBox instead of a Button. Because I wanted to get the selected behavior where CheckBox has it. See below for the modified "DataTemplate_Level2"
<DataTemplate x:Key="DataTemplate_Level2" DataType="{x:Type local:Seat}">
<CheckBox Content="{Binding SeatNumber}" Height="40" Width="50" Margin="4" Style="{StaticResource CheckBoxStyle}"
IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
Step 3: I have modified appearance of the CheckBox. So that it does not appear like a checkbox but looks like a button (you can still customize to look like a real seat). See below for my modified CheckBoxStyle
<Style x:Key="CheckBoxStyle" TargetType="CheckBox">
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Border x:Name="MainBorder" BorderBrush="Red" BorderThickness="1">
<TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=CheckBox}, Path=Content}"
TextAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="MainBorder" Property="Background" Value="Yellow" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Step 4: I have added some buttons and textblock to my Window to test for the selected seats (This piece of code is for testing purpose only). See below my rest of the xaml.
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<ItemsControl x:Name="lst" ItemTemplate="{DynamicResource DataTemplate_Level1}" ItemsSource="{Binding TopSeatList}"/>
<Button Content="Get Selected Seat Numbers" Height="40" Width="50" Margin="4,4,4,4" Grid.Row="1" Click="Button_Click"/>
<TextBlock x:Name="SelectedSeatNumbersTextBlock" Grid.Row="2" />
</Grid>
Note:- There is no modification for "DataTemplate_Level1". Hence you can still copy-paste of yours.
Step 5: Now in my main window view model, I have added a list like your List< List > and populated some dummy data.
public List<List<Seat>> TopSeatList
{
get => topSeatList;
set
{
topSeatList = value;
OnPropertyChanged("TopSeatList");
}
}
Step 6: In the code-behind for the Button_Click event I did to get the selected seat numbers and display in a textblock.
private void Button_Click(object sender, RoutedEventArgs e)
{
var selectedSeats = selectSeatsViewModel.TopSeatList.SelectMany(x => x.Where(y => y.IsSelected));
string selectedSeatNumbers = string.Empty;
foreach(var seat in selectedSeats)
{
selectedSeatNumbers += seat.SeatNumber + "";
}
SelectedSeatNumbersTextBlock.Text = selectedSeatNumbers;
}
Note:- I consider to implement much better approach above click event with a command so that you can avoid it writing it in code-behind.
I hope this helps you to move forward with your solution. Please give a try and let us know results. Feel free to post your questions.

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();
}
}

Change button content MVVM

I have a button with the following content:
<StackPanel Orientation="Horizontal">
<TextBlock Text="Connect"/>
<materialDesign:PackIcon Kind="Arrow"/>
</StackPanel>
I searched and found this: WPF Button content binding but I'm not sure how to apply the solution when I have all of the three: a Stackpanel, the PackIcon (object) and the Textblock.
I have this progressBar which I make it appear under the button:
<ProgressBar x:Name="XZ" Foreground="Black" Grid.Row="4" Grid.Column="1"
Visibility="{Binding Connecting, UpdateSourceTrigger=PropertyChanged, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"
Value="50"
IsIndeterminate="True" />
I want to make it so when I click the button, instead of showing the ProgressBar where it is right now, to basically remove the Text and the PackIcon and place the ProgressBar in the button.
Actually changing in the controls could be done with Data Triggers; though that seems a bit over the top in this case.
I would just toggle the visibility of two controls:
<Grid>
<StackPanel Orientation="Horizontal" Visibility="{Binding Connecting, Converter={StaticResource BooleanToCollapsedConverter}}"">
<TextBlock Text="Connect"/>
<materialDesign:PackIcon Kind="Arrow"/>
</StackPanel>
<ProgressBar x:Name="XZ" Foreground="Black" Grid.Row="4" Grid.Column="1"
Visibility="{Binding Connecting, Converter={StaticResource BooleanToVisibilityConverter}}"
Value="50"
IsIndeterminate="True" />
</Grid>
That would be the content of your button. BooleanToCollapsedConverter is just the inverse of a VisibiltyToBooleanConverter; there are a number of ways to do it and is left as an exercise.
As an aside; UpdateSourceTrigger doesn't make any sense on a OneWay binding (it doesn't update the source!) and you don't even need that on visibility as that's not an input the user can change.
You could use a data template. Something like:
XAML:
<Window.Resources>
<DataTemplate DataType="{x:Type local:ButtonInfo}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="Press me"></Label>
<Label Grid.Row="1" Content="{Binding Label}"></Label>
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ProgressInfo}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<ProgressBar Height="30" Value="{Binding Progress}"></ProgressBar>
<Label Grid.Row="1" Content="{Binding Label}"></Label>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<Button Command="{Binding ProcessCommand}" Content="{Binding ButtonInfo}">
</Button>
</Grid>
C#:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
public class ViewModelBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class MainWindowViewModel:ViewModelBase
{
public MainWindowViewModel()
{
ButtonInfo = new ButtonInfo(){Label = "Button Info"};
ProcessCommand = new DelegateCommand(Process);
}
private ButtonInfo _buttonInfo;
public ButtonInfo ButtonInfo
{
get { return _buttonInfo; }
set
{
_buttonInfo = value;
OnPropertyChanged();
}
}
public DelegateCommand ProcessCommand { get; set; }
private async void Process()
{
ButtonInfo = new ProgressInfo(){Label = "Progress Info"};
await ProcessAsync();
}
private Task ProcessAsync()
{
return Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
Application.Current.Dispatcher.Invoke(() =>
{
ButtonInfo.Progress = i;
if (i==99)
{
ButtonInfo = new ButtonInfo(){Label = "Button Again"};
}
});
Thread.Sleep(100);
}
});
}
}
public class ButtonInfo:ViewModelBase
{
private string _label;
private int _progress;
private bool _isProcessing;
public string Label
{
get { return _label; }
set
{
_label = value;
OnPropertyChanged();
}
}
public int Progress
{
get { return _progress; }
set
{
_progress = value;
OnPropertyChanged();
}
}
public bool IsProcessing
{
get { return _isProcessing; }
set
{
_isProcessing = value;
OnPropertyChanged();
}
}
}
public class ProgressInfo : ButtonInfo { }
You can create a template for the button to achieve this, then reuse the template everywhere you want a button with loading as following:
<Button Width="120" Height="40" Tag="False" Name="loadingButton" Click="loadingButton_Click">
<Button.Template>
<ControlTemplate>
<Border Name="PART_Border" BorderBrush="Black" BorderThickness="1" CornerRadius="2" Background="Transparent">
<Grid Name="PART_Root">
<TextBlock Name="PART_Text" HorizontalAlignment="Center" VerticalAlignment="Center">Data</TextBlock>
<ProgressBar IsIndeterminate="True" Name="PART_Loading"></ProgressBar>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Tag" Value="True">
<Setter TargetName="PART_Text" Property="Visibility" Value="Collapsed"></Setter>
<Setter TargetName="PART_Loading" Property="Visibility" Value="Visible"></Setter>
</Trigger>
<Trigger Property="Tag" Value="False" >
<Setter TargetName="PART_Text" Property="Visibility" Value="Visible"></Setter>
<Setter TargetName="PART_Loading" Property="Visibility" Value="Collapsed"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
And the event for button click would be:
private async void loadingButton_Click(object sender, RoutedEventArgs e)
{
loadingButton.Tag = true.ToString();//display loading
await Task.Run(() => { Thread.Sleep(4000); });//fake data loading
loadingButton.Tag = false.ToString();//hide loading
}
Note that you can also bind the Tag property for a property inside your view model if you where using MVVM pattern.

Autosize column in ListView (UWP)

I would like to arrange my items in a ListView with a Text and an image like in this example:
The first column is automatic, so it expands to the widest element.
The second column should expand to fill the remaining space.
So, how can I make an Auto column... or simulate it?
Obviously, the difficult part of this layout is that different elements have their widths depending on other element's widths.
EDIT: Using #WPInfo's answer, I've tried with this:
XAML
<Page.DataContext>
<local:MainViewModel></local:MainViewModel>
</Page.DataContext>
<ListView ItemsSource="{Binding Items}">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding}" />
<Border Background="CornflowerBlue" Grid.Column="1" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
CODE:
public class MainViewModel
{
public IList<string> Items { get; }
public MainViewModel()
{
Items = new List<string>()
{
"ABC",
"ABCDEF",
"ABCDEFGHI",
};
}
}
The expected result was:
However, the actual result is:
The goal is that the first column to be as wide as the widest element inside it.
In WPF, this is solved easily with the use of Grid.SharedSizeGroup. In UWP there is not such a thing.
The problem is that ListView children don't know anything about each other. There's no simple trick to have all the items know what the longest text item is, so a more complicated solution is required. The following works. It will determine a fixed TextBlock size based on the largest item from the string list, and it will update it should the font size change. Obviously a simpler solution closer resembling your example code can be had if you decide on a fixed column size for either the text or the image, but I assume you already know that.
XAML:
<Page.DataContext>
<local:MainViewModel></local:MainViewModel>
</Page.DataContext>
<ListView ItemsSource="{Binding Items}">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding MyText, Mode=OneWay}" FontSize="{Binding ItemFontSize, Mode=OneWay}" Width="{Binding TextWidth, Mode=OneWay}" />
<Border Background="CornflowerBlue" Grid.Column="1" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
C# ViewModel
public class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<MyListViewItemsClass> Items { get; set; }
private List<string> ItemsFromModel;
private int _myFontSize;
public int MyFontSize
{
get { return _myFontSize; }
set
{
if (value != _myFontSize)
{
_myFontSize = value;
OnPropertyChanged("MyFontSize");
// Font size changed, need to refresh ListView items
LoadRefreshMyListViewItems();
}
}
}
public MainViewModel()
{
// set default font size
_myFontSize = 20;
// example data
ItemsFromModel = new List<string>()
{
"ABC",
"ABCDEF",
"ABCDEFGHI",
};
LoadRefreshMyListViewItems();
}
public void LoadRefreshMyListViewItems()
{
int itemMaxTextLength = 0;
foreach (var modelItem in ItemsFromModel)
{
if (modelItem.Length > itemMaxTextLength) { itemMaxTextLength = modelItem.Length; }
}
Items = new ObservableCollection<MyListViewItemsClass>();
// Convert points to pixels, multiply by max character length to determine fixed textblock width
// This assumes 96 DPI. Search for how to grab system DPI in C# there are answers on SO.
double width = MyFontSize * 0.75 * itemMaxTextLength;
foreach (var itemFromModel in ItemsFromModel)
{
var item = new MyListViewItemsClass();
item.MyText = itemFromModel;
item.ItemFontSize = MyFontSize;
item.TextWidth = width;
Items.Add(item);
}
OnPropertyChanged("Items");
}
public class MyListViewItemsClass
{
public string MyText { get; set; }
public int ItemFontSize { get; set; }
public double TextWidth { get; set; }
}
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, e);
}
public void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Try setting ItemContainerStyle property for the ListView:
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
Then your ListView may look like this:
<ListView x:Name="list">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock ... />
<Image Grid.Column="1" HorizontalAlignment="Right" ... />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
After that, you can set a proper value for HorizontalAlignment property of your ListView.

Categories

Resources