wpf responsive ListBox / Grid - c#

I am trying to recreate the following responsive grid (see 'Desired behavior' link below) in WPF. But, I am struggling to find the best way to achieve this.
Ideally, I would like a horizontal list of tiles that grow and shrink in size to fit the available space. As a starting point, I have a list box that wraps, but i'm left with white space when re-sizing. Any pointers would be appreciated.
Current Wrap Panel:
:
Desired behavior
My current Code:
<Window x:Class="WrappingListbox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="Wrapping Listbox"
Width="525"
Height="350"
mc:Ignorable="d">
<Grid>
<ListBox x:Name="listbox1" ScrollViewer.VerticalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" Orientation="Vertical" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="20" HorizontalAlignment="Center">
<Viewbox>
<Grid x:Name="backgroundGrid"
Width="60"
Height="60">
<Rectangle x:Name="Rect" Fill="green" />
</Grid>
</Viewbox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<WrapPanel HorizontalAlignment="Left" VerticalAlignment="Top" />
</Grid>

this will solve the size problem for you (by binding the dimensions of the item to the dimensions of the listbox but I think you need to work more on it but here is just the start )
// add this in your window definition
xmlns:Local="clr-namespace:WpfApplication2"
//then here is your listbox
<ListBox x:Name="listbox1" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<ListBox.Resources>
<Local:DimentionConverter x:Key="Converter" />
</ListBox.Resources>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" Orientation="Vertical" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="20" HorizontalAlignment="Center" VerticalAlignment="Center" >
<Viewbox>
<Grid x:Name="backgroundGrid"
Height="{Binding RelativeSource={RelativeSource AncestorType=ListBox},Path=ActualWidth,Converter={StaticResource ResourceKey=Converter},ConverterParameter=5}"
Width="{Binding RelativeSource={RelativeSource AncestorType=ListBox},Path=ActualWidth,Converter={StaticResource ResourceKey=Converter},ConverterParameter=5}" >
<Rectangle x:Name="Rect" Fill="green" />
</Grid>
</Viewbox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
public class DimentionConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
return (double)value / double.Parse(parameter as string);
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return null;
}
}
Important Notice : you can't do it without the converter you have to use it to decide how big you want your item compared to the hosting listbox ofcourse you can change the value as you wish I made it 5 as an example

Related

WPF: Synchronize width of all items in ItemsControl

Is it possible to adjust the width of all TextBlocks in the WrapPanel to the size of the largest TextBlock within the WrapPanel? The end result should be that the width of the control containing "Some data" has the same width as the control containing "Even more data than before." I have attached to my initial code as a starting point. I have used strings as an example, but the collection data and template could be anything, so I can't rely on the length of the string.
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:System="clr-namespace:System;assembly=mscorlib"
xmlns:Collections="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Collections:ArrayList x:Key="data">
<System:String>Some data</System:String>
<System:String>Some more data</System:String>
<System:String>Even more data than before</System:String>
</Collections:ArrayList>
</Window.Resources>
<ItemsControl ItemsSource="{StaticResource data}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel></WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="5" BorderThickness="1" BorderBrush="Black">
<TextBlock Text="{Binding}"></TextBlock>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
And an image of the desired output:
Use a shared size grid:
<ItemsControl ItemsSource="{StaticResource data}" Grid.IsSharedSizeScope="True">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel></WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="ColumnSize" />
</Grid.ColumnDefinitions>
<Border Margin="5" BorderThickness="1" BorderBrush="Black">
<TextBlock Text="{Binding}"></TextBlock>
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
All columns are guaranteed to be the same width as they share a size group. As they are sized auto, they will also size to the largest instance of any of the grid's content.
You have to create your own custom version of a wrappanel that is derived from wrappanel to achieve what you want.
Normally for a custom panel you have to override following 2 methods :
MeasureOverride
ArrangeOverride
In your case you need measueoverride where you will iterate over the elements and find out which one is the biggest and then use that same size for all elements in arrangeoverride.
Some more info on how to create a custom panel :
http://www.wpftutorial.net/CustomLayoutPanel.html
You can add HorizontalContentAlignment=Stretch and Add UniformGrid in itemsPanel.
<Window.Resources>
<Collections:ArrayList x:Key="data">
<System:String>Some data</System:String>
<System:String>Some more data</System:String>
<System:String>Even more data than before</System:String>
</Collections:ArrayList>
</Window.Resources>
<ListBox ItemsSource="{StaticResource data}" HorizontalContentAlignment="Stretch">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid IsItemsHost="True"></UniformGrid>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Border Margin="5" BorderThickness="1" BorderBrush="Black" >
<TextBlock Text="{Binding}" Width="{Binding
RelativeSource={RelativeSource TemplatedParent},
Path=Width}"></TextBlock>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This is an alternative to Philip Stuyck's custom panel suggestion that you may find more straightforward for your particular scenario. (It is a bit of a hack admittedly.)
You can calculate the length of a string using the FormattedText class. So you could iterate over your strings and calculate the maximum length (this also assumes that you know the font family and size). Then just bind the width of your text blocks to the maximum width. I would store the width value in a single property at the parent level, then using a RelativeSource binding:
<TextBlock Text="{Binding}"
Width="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl},
Path=MaximumWidth}}"
/>
(One drawback of this approach is that if the items collection changes, you'll have to recalculate MaximumWidth.)

Create buttons programmatically with unique id

I have created some buttons by first filling a class containing get/set methods and then using that info in XAML.
C#
List<MediaDetail> movies = new List<MediaDetail>();
...
...
MovieListView.ItemsSource = movies;
XAML
<Window.Resources>
<DataTemplate x:Key="ItemTemplate">
<WrapPanel Orientation="Vertical" Width="Auto">
<Button Width="200" Height="300" Click="SelectMovie_Click" Name ="NEED THIS TO BE DYNAMIC">
<Button.Template>
<ControlTemplate>
<Image Source="{Binding image}"/>
</ControlTemplate>
</Button.Template>
</Button>
<TextBlock Text="{Binding title}" HorizontalAlignment="Center"/>
</WrapPanel>
</DataTemplate>
</Window.Resources>
<ListView Name="MovieListView" ItemTemplate="{StaticResource ItemTemplate}" ItemsSource="{Binding Path = movies}" Margin="0,0,0,0" SelectionChanged="MovieListView_SelectionChanged" Grid.Row="1">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="5" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
However the issue is, I need each button to have a unique id. I read elsewhere that this can't be done in XAML but I'm not sure how or where in my C# code to create these buttons.
I think the most flexible solution here would be to define a behavior:
public class UniqueNameBehavior : Behavior<FrameworkElement>
{
protected override void OnAttached()
{
base.OnAttached();
//assign unique name to the associated element, for example:
AssociatedObject.Name = Guid.NewGuid().ToString().Replace("-", null);
}
}
In XAML, attach this behavior to any element:
<Button>
<i:Interaction.Behaviors>
<local:UniqueNameBehavior/>
</i:Interaction.Behaviors>
</Button>
where xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" from System.Windows.Interactivity assembly.

ItemsControl view doesn't updated when an item in ObservableCollection is updated

Introduction :
Hi, I meet a weird problem here. My ItemsControl doesn't update the view if I am updating the model (e.g. changing the value of IsSelected).
One of IsSelected purpose however, is, if the value is true, then the Background of the MusicalNotationBox (UserControl) is changed to blue, and if it's false then it's changed back to transparent.
A lot of person asked : Why not using trigger such as Focus for IsSelected? Because it's not just
for "visual" purposes. I have a Command which to change certain
Property (for example Note's Octave) of each MusicalNotation
object in VM's MusicalNotations, which IsSelected==true (support
multiselection), so I think I need IsSelected in the model. But this is not the problem here, so please no answer solely on this matter.
The problem is :
The model property is changed successfully (checked and verified), but it seems that the view isn't.
If I use the singular form of the UserControl, for e.g. <c:MusicalNotationBox DataContext={Binding}/> (ofc along with it's friends a.k.a correct properties in the VM), it synced perfectly. So, I'm not quite sure where the problem lies with the OC.
UPDATE : If I, for example create a MouseBinding that each time I click, then a new MusicalNotation added to the list, then, the view is also updated.
I have read several topic (google and here) on "Observable Collection
doesn't update the ItemsControl`, but still found no satisfying answer.
Here's my code (code may trimmed (...) for clarity sake) :
MODEL
public class MusicalNotation : ... INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
...
private bool _isSelected;
...
public bool IsSelected
{
get { return _isSelected; }
set { _isSelected = value; NotifyPropertyChanged("IsSelected"); }
}
...
public MusicalNotation()
{
...
IsSelected = false;
}
...
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
VIEW MODEL
public class MainWindowModelView : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<MusicalNotation> _musicalNotations;
...
public ObservableCollection<MusicalNotation> MusicalNotations
{
get { return _musicalNotations; }
set { _musicalNotations = value; NotifyPropertyChanged("MusicalNotations"); }
}
public MainWindowModelView()
{
...
MusicalNotations = new ObservableCollection<MusicalNotation>();
//Direct initialization for testing purpose
MusicalNotations.Add(MusicalNotation.GetEmptyNote(new TimeSignature(4, 4)));
MusicalNotations.Add(MusicalNotation.GetEmptyNote(new TimeSignature(4, 4)));
foreach (MusicalNotation item in MusicalNotations)
{
item.IsSelected = true;
}
}
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
VIEW
<Window ...>
<Window.Resources>
</Window.Resources>
<Window.InputBindings>
...
</Window.InputBindings>
<Grid>
...
<ItemsControl Grid.Column="0" Grid.Row="0" ItemsSource="{Binding MusicalNotations, Mode=OneWay}"
HorizontalAlignment="Center" VerticalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel IsItemsHost="True" Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<c:MusicalNotationBox/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Thanks.
UPDATE, My MusicalNotationBox XAML
<UserControl x:Class="NumberedMusicScoresUserControl.MusicalNotationBox"
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:NumberedMusicScoresUserControl.MusicalNotationBoxProperties"
mc:Ignorable="d">
<UserControl.Resources>
<local:DotConverter x:Key="DotConverter"/>
<local:NoteConverter x:Key="NoteConverter"/>
<local:AccidentalConverter x:Key="AccidentalConverter"/>
<local:IsSelectedConverter x:Key="IsSelectedConverter"/>
</UserControl.Resources>
<Grid x:Name="grid" Margin="10,5,10,5"
HorizontalAlignment="Center" VerticalAlignment="Center"
Background="{Binding Path=MusicalNotation.IsSelected, Converter={StaticResource IsSelectedConverter}, Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="1"
Text="b"
Visibility="{Binding Path=MusicalNotation.Accidental, Converter={StaticResource AccidentalConverter}, ConverterParameter=FL, Mode=OneWay}"
FontSize="15" FontFamily="CourierNew"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Path Grid.Column="1" Grid.Row="1" Stroke="Black" StrokeThickness="1" Stretch="Fill"
Visibility="{Binding Path=MusicalNotation.Accidental, Converter={StaticResource AccidentalConverter}, ConverterParameter=SP, Mode=OneWay}" >
<Path.Data>
<LineGeometry StartPoint="1,0" EndPoint="0,1">
<LineGeometry.Transform>
<RotateTransform CenterX="0" CenterY="0" Angle="30"/>
</LineGeometry.Transform>
</LineGeometry>
</Path.Data>
</Path>
<TextBlock Grid.Column="1" Grid.Row="1"
Text="{Binding Path=MusicalNotation.Note, Converter={StaticResource NoteConverter}, Mode=OneWay}"
FontSize="15" FontFamily="CourierNew"
HorizontalAlignment="Center" VerticalAlignment="Center"
Margin="2.5,0,2.5,0"/>
<ItemsControl Grid.Column="1" Grid.Row="0"
ItemsSource="{Binding Path=MusicalNotation.Octave, Converter={StaticResource DotConverter}, ConverterParameter=TOP, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse HorizontalAlignment="Center" VerticalAlignment="Top"
Margin="{Binding Margin}" Fill="{Binding Fill}"
Width="{Binding Diameter}" Height="{Binding Diameter}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl Grid.Column="1" Grid.Row="2"
ItemsSource="{Binding Path=MusicalNotation.Octave, Converter={StaticResource DotConverter}, ConverterParameter=BOT, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse HorizontalAlignment="Center" VerticalAlignment="Bottom"
Margin="{Binding Margin}" Fill="{Binding Fill}"
Width="{Binding Diameter}" Height="{Binding Diameter}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl Grid.Column="2" Grid.Row="1"
ItemsSource="{Binding Path=MusicalNotation.Dot, Converter={StaticResource DotConverter}, ConverterParameter=RIGHT, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse HorizontalAlignment="Left" VerticalAlignment="Center"
Margin="{Binding Margin}" Fill="{Binding Fill}"
Width="{Binding Diameter}" Height="{Binding Diameter}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
(I didn't include it since, I think, if the "singular" one is correct, then my UserControl is correct as well. Might be wrong though.)
The DataContext for your user control is the MusicalNotation object, so while binding instead of using
Background="{Binding Path=MusicalNotation.IsSelected, Converter={StaticResource ....
just use
Background="{Binding Path=IsSelected, Converter={StaticResource ....
Drop the MusicalNotation prefix from the bindings.
{Binding Path=IsSelected, ...
As you haven't specified any binding for the selected item or for the IsSelected in your xaml, that's why view is not being updated when the property is changed in the VM. For updating the view you will have to provide a binding for SelectedItem property of ItemsControl.

Xaml code explanation

I am working on c# xaml using silverlight and i am bit confused about the hierarchy of this xaml code:
<UserControl.Resources>
<this:MyValueConverter x:Key="TabConverter"/>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="Green">
<ItemsControl ItemsSource="{Binding Path=TabList, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas></Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Width="150" Background="red" Height="100" Canvas.Left="{Binding TabList, Mode=TwoWay, Converter={StaticResource TabConverter}}" Canvas.Top="100" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
When i run it gives a big green color window(sorry if technical names are not correct), whereas it should also show red color somewhere in same window as it has Border Width="150" Background="red" .Could some one please explain me what this code is doing ?
ItemsControl really just binds to a list and applies a datatemplate (border with background red) for each item in the list. As for the reason you're only seeing green, well there's probably nothing in the TabList property on your viewmodel. That way, nothing in the items template renders, and all you see is green.
You'll need to make sure that TabList is bound correctly (it exists on your datacontext, whether that's a view model or not) and that it has items in it.
Here's a simpler version of what you may want to accomplish:
<Grid x:Name="LayoutRoot" Background="Green">
<ItemsControl ItemsSource="{Binding Path=TabList, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Width="150" Background="red" Height="100" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>

Add extra element to ItemsControl's Canvas

I have an ItemsControl set up like this:
<Grid>
<ItemsControl ItemsSource="{Binding Asteroids}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Black">
<!-- i want to add another polygon to the canvas-->
<!--<Polygon Name ="ShipPolygon" Points="{Binding Ship}" Fill="Blue" />-->
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Polygon Fill="Gray" Points="{Binding AsteroidPoints}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
As you can see, the ItemsControl is displaying a collection's elements as Polygons in a Canvas. But i also want to add another Polygon to this Canvas, here named "ShipPolygon". I can't do that this way, because i get an XMLParseException. What is the proper method to do this? Thanks in advance!
You are using an ItemsControl, which is there to display multiple similar items on an itemspanel.
Now obviously you can't add something to your ItemsPanelTemplate there you are only telling the itemscontrol which panel it should use to display its items.
Your ItemsSource and your ItemTemplate suggest that you only want to show Asteroids in your ItemsControl. So the easiest way would be to just overlay your ship onto the itemscontrol with your asteroids
<Grid>
<ItemsControl ItemsSource="{Binding Asteroids}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Black"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Polygon Fill="Gray" Points="{Binding AsteroidPoints}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Polygon Name ="ShipPolygon" Points="{Binding Ship}" Fill="Blue" />
</Grid>
Otherwise you could add it aswell to the itemscontrol, but then need to use different ItemTemplates. And you need to handle, that your ItemsControl no longer only holds Asteroids.
Using implicit item Templates
<!-- No key, so it is implicit and automatically used-->
<DataTemplate DataType="{x:Type Asteroids}">
<Polygon Fill="Gray" Points="{Binding AsteroidPoints}" />
</DataTemplate>
<DataTemplate DataType="{x:Type Player}">
<Polygon Name ="ShipPolygon" Points="{Binding Ship}" Fill="Blue" />
</DataTemplate>
<!-- above must be in a resource section, like app.xaml -->
<Grid>
<ItemsControl ItemsSource="{Binding Entities}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Black"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
Or you could use a DataTemplateSelector.

Categories