WPF - UserControl constructing performance (very poor) - c#

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.

Related

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.

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.

How to have a dynamic DataTemplateSelector

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!

Binding WPF Control Name to different Control

Have style for a series of buttons btn1, btn2, btn3, etc.
Inside the style for each button is a TextBlock for displaying the "Content" of the button, (since the border inside the style covers any content of the button itself).
Now, I would like for the TextBlock name to be tied to the button name. For example - btn1's text block's name would be btn1Txt. The purpose of this would be the end user can assign each button its own text in a settings menu.
Any hints on how I would go about this? I admit I'm relatively new to WPF and bindings.
EDIT:::: WHAT I"VE GOT SO FAR THAT IS WORKING.
On load, the program checks the settings file for the Text for each button. Each button's content is assigned the proper information. Then inside the style, I bind the TextBlock Text to the content of the parent button.
This may not be the normal way of going about it, but it works
Method
List<string> MainButtons = Properties.Settings.Default.MainButtonNames.Cast<string>().ToList();
for (int i = 0; i < MainButtons.Count(); i++)
{
string actualNum = Convert.ToString((i + 1));
var MainButtonFinder = (Button)this.FindName("MainButton" + actualNum);
Console.WriteLine(MainButtonFinder.Name);
MainButtonFinder.Content = MainButtons[i];
Console.WriteLine(MainButtonFinder.Content);
}
Style
<Style x:Key="MainButtonStyle" TargetType="{x:Type Button}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="Width" Value="100px"/>
<Setter Property="MinHeight" Value="50"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border CornerRadius="20" Height="45" Width="100" Margin="0" Background="#FF99CCFF">
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Content}" HorizontalAlignment="Center" VerticalAlignment="Center" FontFamily="LCARS" Foreground="White" Padding="5px" FontSize="18px" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>`
This is the wrong way to go about what you're trying to do. Here's the "right" way to do it. There's a fair amount of boilerplate code here, but you get used to it.
Write a button viewmodel and give your main viewmodel an ObservableCollection of those:
#region ViewModelBase Class
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
#endregion INotifyPropertyChanged
}
#endregion ViewModelBase Class
#region MainViewModel Class
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
ButtonItems.Add(new ButtonItemViewModel("First Command", "First Item", () => MessageBox.Show("First Item Executed")));
ButtonItems.Add(new ButtonItemViewModel("Second Command", "Second Item", () => MessageBox.Show("Second Item Executed")));
}
#region ButtonItems Property
public ObservableCollection<ButtonItemViewModel> ButtonItems { get; }
= new ObservableCollection<ButtonItemViewModel>();
#endregion ButtonItems Property
}
#endregion MainViewModel Class
#region ButtonItemViewModel Class
public class ButtonItemViewModel : ViewModelBase
{
public ButtonItemViewModel(String cmdName, String text, Action cmdAction)
{
CommandName = cmdName;
Text = text;
Command = new DelegateCommand(cmdAction);
}
#region Text Property
private String _text = default(String);
public String Text
{
get { return _text; }
set
{
if (value != _text)
{
_text = value;
OnPropertyChanged();
}
}
}
#endregion Text Property
#region CommandName Property
private String _commandName = default(String);
public String CommandName
{
get { return _commandName; }
private set
{
if (value != _commandName)
{
_commandName = value;
OnPropertyChanged();
}
}
}
#endregion CommandName Property
public ICommand Command { get; private set; }
}
#endregion ButtonItemViewModel Class
public class DelegateCommand : ICommand
{
public DelegateCommand(Action action)
{
_action = action;
}
private Action _action;
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_action?.Invoke();
}
}
Make that MainViewModel the DataContext of your Window:
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
And here's how you can put it all together in the XAML:
<Grid>
<StackPanel Orientation="Vertical">
<GroupBox Header="Buttons">
<ItemsControl ItemsSource="{Binding ButtonItems}" HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button
Margin="2"
MinWidth="80"
Content="{Binding Text}"
Command="{Binding Command}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
<GroupBox Header="Edit Buttons">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox
Grid.Column="0"
Margin="2"
x:Name="ButtonEditorListBox"
ItemsSource="{Binding ButtonItems}"
HorizontalAlignment="Left"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2"
Text="{Binding CommandName}"
/>
<TextBlock
Margin="2"
Text="{Binding Text, StringFormat=': "{0}"'}"
/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
<ContentControl
Grid.Column="1"
Margin="8,2,2,2"
Content="{Binding SelectedItem, ElementName=ButtonEditorListBox}"
>
<ContentControl.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock
Margin="2"
HorizontalAlignment="Stretch"
FontWeight="Bold"
Text="{Binding CommandName, StringFormat={}{0}: }"
/>
<TextBox
Margin="2"
HorizontalAlignment="Stretch"
Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
/>
</StackPanel>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</GroupBox>
</StackPanel>
</Grid>
Back to your question:
Inside the style for each button is a TextBlock for displaying the "Content" of the button, (since the border inside the style covers any content of the button itself).
You're doing styles wrong. Very, very wrong. I can help you fix it if you show me the style.
As I understand what you want is to modify the "text" of the button, it occurs to me that you can do it this way.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBox Text="{Binding ElementName=ButtonTest, Path=Content, UpdateSourceTrigger=PropertyChanged}" ></TextBox>
<Button Name="ButtonTest" Grid.Row="1" Width="100" Height="40">
<Button.ContentTemplate>
<DataTemplate>
<TextBlock Foreground="Blue" Text="{Binding}"></TextBlock>
</DataTemplate>
</Button.ContentTemplate>
</Button>
</Grid>

WPF DropDownButton IsOpen binding breaks after opening drop down

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 = "";
}
}

Categories

Resources