Reevaluating UniformGrid rows and Columns WPF - c#

I'm trying to create a video player that can play multiple videos at once. I've gotten players working and now I'm trying to allow a user to add additional videos at run time. I'm displaying videos in a uniform grid, which uses a converter to decide how many rows and columns it should generate based on the count of videos. It works fine when you define how many players there are before running, however when I add a player while its running the uniform grid doesn't update the rows or columns. It simply adds another video to whatever structure it had before. Is there anyway that I can force it to reevaluate the rows/columns?
TL;DR: Can I reevaluate the rows and columns of a uniform grid while viewing it? How ?
The xaml for the control is below.
Additional Infomation That Might be helpful:
1. Players is an ObservableCollection
2. The Control is being presented with a TabControl
<UserControl x:Class="Views.AllVideos"
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-Views"
mc:Ignorable="d">
<ItemsControl ItemsSource="{Binding Players}"
x:Name="AllVideosControl">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding Players, Converter={StaticResource CountToColumns}, Mode=OneWay}"
Rows="{Binding Players, Converter={StaticResource CountToRows}, Mode=OneWay}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="{DynamicResource AccentBrush}"
BorderThickness="1"
Margin="5">
<ls:PanelPreview DataContext="{Binding}"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>

Setting Rows and Columns isn't needed at all. Everything will work without them too. UniformGrid will place everything uniformly inside whatever container.
If still you want to use Binding , then following works for me , Cols are fixed here. Converters are not need.
void Players_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
Rows = (int)Math.Ceiling(Players.Count / Cols);
}

The problem appears to be that nothing is notifying the binding that the number of items in Players has changed: The Binding is sitting there waiting for your viewmodel to tell it there's a new Players collection.
But since Players is an ObservableCollection, it'll raise PropertyChanged when it's Count changes. So, simplest case:
<UniformGrid
Rows="{Binding Players.Count}"
...
/>
The following works for me to update the row and column counts as items are added to Players. The arithmetic in the converter could use a little work, but the point is that the bindings update UniformGrid.Rows and UniformGrid.Columns every time Players changes. This works because ObservableCollection raises PropertyChanged("Count") every time an item is added or removed. Because I'm binding to a property of an ObservableCollection, the binding subscribes to the ObservableCollection's PropertyChanged event, so it knows to update when Count changes.
C#
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void AddButton_Click(object sender, RoutedEventArgs e)
{
(DataContext as ViewModel).Players.Add($"Player {(DataContext as ViewModel).Players.Count + 1}");
}
private void RemoveButton_Click(object sender, RoutedEventArgs e)
{
if ((DataContext as ViewModel).Players.Count > 0)
(DataContext as ViewModel).Players.RemoveAt(0);
}
}
public class ViewModel
{
private ObservableCollection<String> _players = new ObservableCollection<string>();
public ObservableCollection<String> Players
{
get { return _players; }
}
}
public class RowsColumnsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return Math.Ceiling(Math.Sqrt((int)value));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
XAML
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ItemsControl
Grid.Row="1"
ItemsSource="{Binding Players}"
>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid
Rows="{Binding Players.Count, Converter={StaticResource RowsColumns}}"
Columns="{Binding Players.Count, Converter={StaticResource RowsColumns}}"
/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<Button
Content="Add Player"
Click="AddButton_Click"
Margin="2"
Width="100"
/>
<Button
Content="Remove Player"
Click="RemoveButton_Click"
Margin="2"
Width="100"
/>
</StackPanel>
</Grid>

Related

How can I make this scrollviewer in the grid cell work both horizontaly and verticaly?

I have a grid with some data and one of the cells contains rather long strings (and there could be quite a few). So to not use too much of the available window space, I'd like those strings to be scrollable. Vertically works, but whatever I try, I can't get a horizontal scrollbar.
This is my xaml:
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="150" Width="250">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100"/>
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="0" Grid.Column="0">
<ItemsControl ItemsSource="{Binding Path=BoundTexts}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Text}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</Grid>
</Window>
and here's the code behind to test.
using System.Collections.Generic;
using System.Windows;
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<BoundClass> temp = new List<BoundClass>();
for (int i = 0; i < 10; i++)
{
string t = ""; // just to create some long strings.
for (int j = 0; j < 10; j++) // I know it can be done better.
{
t += $"{j*10:D2}********";
}
temp.Add(new BoundClass(t));
}
BoundTexts = temp.ToArray();
DataContext = this;
}
public BoundClass[] BoundTexts { get; set; }
}
public class BoundClass
{
public string Text { get; set;}
public BoundClass(string text)
{
Text = text;
}
}
}
I know there are a few similar questions on here, but as far as I have seen, they are all shrouded in templates and other complex topics. Also some are answered by "make sure you have a restraining container around it", I think I do by the grid.
Setting ScrollViewer.HorizontalScrollBarVisibility and ScrollViewer.VerticalScrollBarVisibility to "Auto" should do the trick.
Default value for VerticalScrollBarVisibility is ScrollBarVisibility.Visible BUT for HorizontalScrollBarVisibility is ScrollBarVisibility.Disabled. So it should be even enough to set only HorizontalScrollBarVisibility = "Auto"
<ScrollViewer Grid.Row="0" Grid.Column="0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
The proper way to add a ScrollViewer to an ItemsControl is to put it into the ControlTemplate to wrap the ItemsPresenter. This way the ScrollViewer behaves correctly.
If you don't do it this way (and instead wrap the ItemsControl) the ScrollViewer will try to scroll the ItemsControl and not the items!
But you want the ScrollViewer to detect when items of the ItemsControl overflow. Naturally, the ItemsControl itself will stretch to occupy the available space - it won't overflow and will be displayed correctly (fully visible). But the items do overflow the space of the ItemsControl. That's why wrapping the ItemsControl won't work as expected.
Additionally, you must explicitly enable the ScrollViewer.HorizontalScrollBarVisibility. It's important to know that setting it to Auto will impact the performance. It's recommended to set it to Hidden or Visible.
<ItemsControl Height="200">
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<ScrollViewer CanContentScroll="True"
HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Visible">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<!-- Consider to enable virtualization by adding a VirtualizingStackPanel as host Panel.
Important: don't forget to set ScrollViewer.CanContentScroll to 'True' -->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>

Adding dynamically UserControls in WPF/MVVM

Is it possible to add and bind user controls dynamically? Maybe I'll show sample code to show what I exactly mean.
MainWindow:
<UniformGrid
Rows="11"
Columns="11"
DataContext="{StaticResource vm}">
<local:DynamicUserControl
ButClickControl="{Binding Path=UserControlObjects[0].ButClickCommand}"
SomeDataInUserControl="{Binding Path=UserControlObjects[0].SomeData, Mode=OneWay}" />
<local:DynamicUserControl
ButClickControl="{Binding Path=UserControlObjects[1].ButClickCommand}"
SomeDataInUserControl="{Binding Path=UserControlObjects[1].SomeData, Mode=OneWay}" />
<local:DynamicUserControl
ButClickControl="{Binding Path=UserControlObjects[2].ButClickCommand}"
SomeDataInUserControl="{Binding Path=UserControlObjects[2].SomeData, Mode=OneWay}" />
.....
</UniformGrid>
In ViewModel there is an array of UserControlObjects. But in this array I will have over 100 elements, so it is not the best option to write all elements one by one. Is there any way to add DynamicUserControls not in XAML but somewhere in code in loop with keeping the MVVM pattern and binding?
Use an ItemsControl with the UniformGrid as ItemsPanel and the DynamicUserControl in the ItemTemplate:
<ItemsControl DataContext="{StaticResource vm}"
ItemsSource="{Binding UserControlObjects}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="11" Columns="11"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:DynamicUserControl
ButClickControl="{Binding ButClickCommand}"
SomeDataInUserControl="{Binding SomeData}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In my opinion, you would want to keep any controls out of your view model. You could however keep the individual view models that back the controls in a list within the main view model. For example, create the view model that will provide the data for the “dynamic” controls.
class SubViewModel
{
public string Name { get; private set; } = string.Empty;
public SubViewModel(string aName)
{
Name = aName;
}
}
And in the main view model you can do whatever you would do to dynamically create instances. In this case, I am just creating then in a for loop.
class MainWindowViewModel
{
public ObservableCollection<SubViewModel> SubViewModels
{
get
{
return mSubViewModels;
}
} private ObservableCollection<SubViewModel> mSubViewModels = new ObservableCollection<SubViewModel>();
public MainWindowViewModel()
{
for(int i = 0; i < 30; i++)
{
SubViewModels.Add(new SubViewModel($"Control: {i}"));
}
}
}
Then in the view, you can utilize an ItemsControl with an UniformGrid based ItemsPanelTemplate, and then whatever you want for the data template, whether you define it there explicitly, or make a user control (like your local:DynamicUserControl) to clean things up. In this sample, the data template it explicitly defined.
<Window x:Class="ListOfViewsSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ListOfViewsSample"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<ItemsControl ItemsSource="{Binding SubViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Background="LightGray" Margin="10">
<Label Content="{Binding Name}" Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
which results in the following:
If don’t want the multiple dynamic views to be the same, you can look into data template selectors to display something different based on the specified view model, but based in your question I think you were looking for a list of the same control/data. Hope this helps!
The usual way of doing this is:
Create an ItemsControl for the dynamic items you want to create
Override the ItemsPanel to whatever you need (UniformGrid in your case)
Bind it to a list of view models, with one view model per control
Define DataTemplates to map each view model type to its corresponding view type

uwp xaml DataBinding to nested property inside an ObservableCollection

This is my first time building a UWP app and I'm new to c#/Windows in general. I am trying to use a Pivot in the UI. I want the pivot headers to be from an ObservableCollection of usb devices connected, which go by the class Device_Info. Each USB Device has a property called HardwareRevNumMajor that I would like to display as each pivots header. My xaml looks like this:
<Page
x:Class="usb_test.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:usb_test"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:code="using:usb_test"
mc:Ignorable="d">
<Page.DataContext>
<local:View_Model/>
</Page.DataContext>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Pivot x:Name="pivot1" Title="Pivot" Opacity="0.99" ItemsSource="{Binding Devices}">
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<Binding Path="Device_Info.HardwareRevNumMajor"/>
</TextBlock.Text>
</TextBlock>
<!--<TextBlock Text="{Binding HardwareRevNumMajor}">
</TextBlock>-->
</DataTemplate>
</Pivot.HeaderTemplate>
<Pivot.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding DeviceFiles}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding numBlocks}"/>
<TextBlock Text="{Binding syncTime}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
</Grid>
As soon as a Device_Info object gets added to the Devices observableCollection I get an error. Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED)). Then if I click in my MainPage.xaml file in the designer I see this:
TargetException: Object does not match target type.
StackTrace:
at System.Reflection.RuntimeMethodInfo.CheckConsistency(Object target)
at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(Object obj,
BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, Object[] index)
InnerException: None
You can see from the xaml above that I've tried a number of ways to display the HarwareRevNumMajor property that lives on a Device_Info by the commented out TextBlock code.
My ViewModel looks like this:
namespace usb_test
{
public class View_Model
{
public ObservableCollection<Device_Info> _devices;
public ObservableCollection<File_Info> _deviceFiles;
public CoreDispatcher _dispatcher;
public View_Model() {
_devices = new ObservableCollection<Device_Info>();
_deviceFiles = new ObservableCollection<File_Info>();
}
public ObservableCollection<Device_Info> Devices
{
get
{
return _devices;
}
}
public ObservableCollection<File_Info> DeviceFiles
{
get
{
return _deviceFiles;
}
}
In My MainPage.xaml.cs I have this:
namespace usb_test
{
public sealed partial class MainPage : Page
{
Device_List devList = new Device_List();
Device_Structure deviceStruct = new Device_Structure();
View_Model viewModel = new View_Model();
public MainPage()
{
this.InitializeComponent();
viewModel._dispatcher = Dispatcher;
this.DataContext = viewModel;
devList.devices.CollectionChanged += this.OnCollectionChanged;
deviceStruct.deviceFiles.CollectionChanged += this.OnCollectionChanged;
...
This line of code is what adds a device to the list:
await viewModel._dispatcher.RunAsync(CoreDispatcherPriority.Normal, async() => { devList.devices.Add(devInfo); });
This line succeeds and adds a Dev_Info object into the devices observableCollection and shortly after the application crashes.
I'm sure there are more errors when I try to display File Info stuff later in the xaml but I'd really appreciate just getting the pivot header to display correctly. Like I said I'm very new to this so I'm assuming there are a number of problems, I'd appreciate any advice/clarity into what is stopping me from not being able to display a property from a Dev_Info object.
Thanks!
Your items from Devices are of type Device_Info. In your DataTemplate your bind the text property to Device_Info.HardwareRevNumMajor. Since the context is your item (Device_Info), you try to access a property Device_Info.
If your write:
<TextBlock Text="{Binding HardwareRevNumMajor}" />
You try to access the property HardwareRevNumMajor wich probably exists in Device_Info.
Thanks for the suggestions above. I made some progress.
I rewrote my xaml like this:
<Page
x:Class="usb_test.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:usb_test"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:code="using:usb_test"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Pivot x:Name="pivot1" Title="Pivot" Opacity="0.99" ItemsSource="{x:Bind viewModel.Devices}">
<Pivot.HeaderTemplate>
<DataTemplate x:DataType="local:Device_Info">
<TextBlock Text="{x:Bind HardwareRevNumMajor}"></TextBlock>
</DataTemplate>
</Pivot.HeaderTemplate>
<Pivot.ItemTemplate>
<DataTemplate x:DataType="local:Device_Info">
<ListView ItemsSource="{x:Bind devStruct.deviceFiles}" Grid.Row="1">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:File_Info">
<StackPanel Orientation="Horizontal">
<TextBlock Foreground="Red" Text="{x:Bind fileName}" HorizontalAlignment="Center" Margin="0,0,20,0"/>
<TextBlock Foreground="Red" Text="{x:Bind numBlocks}" HorizontalAlignment="Center" Margin="0,0,20,0"/>
<Button Content="Download File" Click="{x:Bind onDownloadClick}" HorizontalAlignment="Center" Margin="0,0,20,0">
</Button>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
</Grid>
You'll notice I took out the
<Page.DataContext>
<local:View_Model/>
</Page.DataContext>
from my previous xaml code.
Then I used ItemsSource="{x:Bind viewModel.Devices}"> in the Pivot. viewModel is defined in my MainPage.xaml.cs and is an instance of the View_Model class I created above. Then from there I added a DataType to DataTemplate which is a Device_Info and then I could easily access an attribute of the Device_Info object, in this case HardwareRevNumMajor.
The PivotItemTemplate is a little more confusing. The class Device_Info has a property on it called devStruct which is a class I created (Device_Struct). An instance of Device_Struct has an ObservableCollection of File_Info objects. So this collection tells us the files on the device and it's called deviceFiles. That is the collection I wanted to use inside the Pivot Items. I'm not sure if there's a way to do this without inserting a <ListView> into the XAML but this is what I found works. With this I can now display information about the files on each Pivot Item.
I also switched from using Binding to x:Binding. To be honest I'm not sure exactly why that helped but when I used Binding I was getting: Object reference not set to an instance of an object. If anyone has a better understanding of that, I'd love to hear about it. Thanks again!

WPF ItemsControl Button command binding not working

I have an ItemsControl control that has an ObservableCollection as its ItemsSource. It also has a button located inside of its DataTemplate. The button's Command property is bound to a RelayCommand in the ViewModel (I'm using MVVM Light) and the CommandParameter is bound to the corresponding item in the ItemsSource.
The problem is that the command never fires, for some reason. Code-behind works fine, on the other hand. When debugging the mouse click event handler I can see that the sender (of type Button) has a CommandParameter filled with the correct data whereas Command is null.
What did I miss here?
XAML:
<ItemsControl ItemsSource="{Binding Users}"
Margin="{StaticResource ContentMargin}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Margin="{StaticResource ImageButtonMargin}"
Style="{StaticResource ImageButtonStyle}"
Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}, Path=DataContext.UserSelectedCommand}"
CommandParameter="{Binding}">
<!--...-->
ViewModel:
private ObservableCollection<User> _users;
private RelayCommand<User> _userSelectedCommand;
public ObservableCollection<User> Users
{
get { return _users; }
set
{
_users = value;
RaisePropertyChanged();
}
}
public RelayCommand<User> UserSelectedCommand
{
get { return _userSelectedCommand; }
}
protected override sealed void SetCommands() // called in the constructor which is in turned called by SimpleIoc
{
userSelectedCommand = new RelayCommand<User>((user) => UserSeriesSelected(user));
}
private void UserSelected(User selectedUser)
{
}
Use named Element binding as binding source inside your data template to access commands from root data context. You can use root grid or other containers as named element. ItemsControl iteself can be used too.
<ItemsControl x:Name="MyItems" ItemsSource="{Binding Users}"
Margin="{StaticResource ContentMargin}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Margin="{StaticResource ImageButtonMargin}"
Style="{StaticResource ImageButtonStyle}"
Command="{Binding ElementName=MyItems, Path=DataContext.UserSelectedCommand}"
CommandParameter="{Binding}">
<!--...-->
You need to add "FindAncestor" to your relative source binding:
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}
In my opinion you should change your strategy, put the command in the User class and from that class notify the view-model through an event.
This is going to simplify your xaml code and, in my opinion, making your view-model more coherent.

How do I get access to the item in a ListBox that is being rendered rather than its source data?

In a continuation from my question here
I tried using the solution provided in the linked answer, however when I give it the image which the ListBox is binding to, it gives me the wrong position, because the Item Template is loading in a source URL rather than the actual image, and it seems the data is null.
My Item Template looks like this:
<ListBox ItemsSource="{Binding Items}"
HorizontalContentAlignment="Left"
VerticalContentAlignment="Center"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.HorizontalScrollBarVisibility="Visible"
Height="64" Name="listBoxSource"
VerticalAlignment="Bottom"
SelectionChanged="listBoxSource_SelectionChanged" Canvas.Left="32" Canvas.Top="365" Width="596"
>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image x:Name="ListImage" Source="{Binding thumbnailURL}" Height="64" Width="64"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
How do I get access to "ListImage" for the currently selected Item?
Every ItemsControl has an ItemsContainerGenerator, which (surprisingly enough) is responsible for generating a control for each item in the list. It provides a few useful methods for finding the container of a given item, and vice versa. You can use it like this:
private void listBoxSource_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listBox = (ListBox) sender;
var containerGenerator = listBox.ItemContainerGenerator;
var container = (UIElement)containerGenerator.ContainerFromItem(listBox.SelectedItem);
}
You can then use the variable container with the solution from your other post to find the coordinates,
Point position = container.GetRelativePosition(Application.Current.RootVisual);
And on a side note, in your DataTemplate you don't need the StackPanel, since the ListBox is providing that with its ItemsPanelTemplate.
<DataTemplate>
<Image x:Name="ListImage" Source="{Binding thumbnailURL}" Height="64" Width="64"/>
</DataTemplate>
on tap event you can do this:
private void image_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
Image selectedImage = e.OriginalSource as Image;
}
is this what you need?
I would suggest a different solution because the way you are trying to solve it (finding the control) is prone to error.
Make the DataTemplate a resource and refer to it in the ListBox by using ItemTemplate="{StaticResource myTemplate}"
Next, when an item is selected, add a ContentControl to the Canvas, set its ContentTemplate to the same DataTemplate and the DataContext to the SelectedItem.
This way you only have to define the template once (one place to maintain it) and do not have to walk the VisualTree.

Categories