VisualBrush (linked to VLC) doesn't change on TabControl selection change - c#

I am having a weird experience with a combination of visual brush (linked to a VLC player through VLC.DotNet) and a tab control. I have created a custom control using the VLC player to watch an RTSP stream and have multiple of these controls in a given window.
The problem is that if I put all the controls in a list view they all display properly. But if I put them in a tab control then it always shows the stream of the first-selected tab item, not matter what tab I'm currently on. Everything else in the control (label, etc.) changes properly, but not the part drawn by the visual brush.
The view for my control is defined as:
<UserControl x:Class="myApp.View.CameraMonitorView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Wpf="clr-namespace:Vlc.DotNet.Wpf;assembly=Vlc.DotNet.Wpf"
Loaded="UserControl_Loaded">
<DockPanel>
<Label Content="{Binding Name}" DockPanel.Dock="Bottom"/>
<Grid Margin="3">
<Grid.Background>
<VisualBrush Stretch="Uniform">
<VisualBrush.Visual>
<Image Source="{Binding VideoSource, ElementName=vlcControl}"/>
<!--<Image Source="{Binding Image}" /> -->
</VisualBrush.Visual>
</VisualBrush>
</Grid.Background>
<Wpf:VlcControl x:Name="vlcControl" Margin="3"/>
</Grid>
</DockPanel>
</UserControl>
The code behind for the view starts playing the RTSP, but I don't think that code will help with this problem.
Meanwhile the ViewModel (stripped down for ease of viewing) is just:
class CameraMonitorViewModel : ViewModelBase
{
public CameraMonitorViewModel(string name, string image)
{
Name = name;
Image = image;
}
public string Name {get; set;}
public string Image { get; set; }
}
And I have a data template defined as:
<DataTemplate DataType="{x:Type vm:CameraMonitorViewModel}">
<v:CameraMonitorView HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</DataTemplate>
The full window view model has an ObservableCollection called Monitors and the view displays:
<TabControl ItemsSource="{Binding Monitors}" SelectedIndex="0" Height="300">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding Name}"/>
<Setter Property="Content" Value="{Binding}"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
<ListView ItemsSource="{Binding Monitors}" Height="300" />
The ListView properly shows different images from each camera. The tab item will always show the same camera, but the control's label item changes when I clicked different tabs. Moreover, if I replace the databinding to the VLC control's image to the commented out static image object (whose source is set by way of the view model) then the image will properly change when I click different tabs.
I'm really confused and would appreciate any help that could be provided.

Yeah, as I noted in the comment to the question the problem was that I was starting the stream playing based on the Loaded event for the control. But it seems like it doesn't get fired after the second tab, probably because of what Rachel mentions here: Loaded event doesn't fire for the 4th TabControl tab (being that it reuses the template rather than loading multiple ones).

Related

How to make listbox placed in a large canvas in wpf more performant?

I have a custom UserControl looks like this:
The brown boxes are ListBoxItems in a ListBox control and there are many such items. Each item again contains a lot of other controls, like images, text blocks among others. They and a Rectangle control are fixed positioned relative to a very big canvas, wrapped in a ScrollViewer. The rectangle takes up almost the entire height of the canvas. Currently, all the boxes are rendered at once, as can be confirmed in the visual tree (in the visual tree, there are 30k+ elements with about 40 elements per ListBoxItem), because the ListBox has a height of almost the canvas' height. However, the user can only see a small portion of all the boxes (and the rectangle) at one time. The user can scroll down to bring the boxes into view and the corresponding part of the rectangle. Since all the boxes are rendered at once, the UserControl behaves very poorly in terms of performance when a view containing this UserControl is being navigated to.
Apparently, the ListBox is not virtualized in this setup. I tried to limit the height of the ListBox to the containing ScrollViewer, and then virtualizing seems to be turned on. However now the ListBox itself has an implicit ScrollViewer in it. When the user scrolls the viewport down, the boxes corresponding to the certain part of the rectangle will not be shown.
The code I use to simulate this:
Window x:Class="ListBoxVirtualizationExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Width="800"
Height="450">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Canvas Width="2000"
Height="2000">
<Rectangle x:Name="Rect1"
Canvas.Left="0"
Canvas.Top="0"
Width="100"
Height="100"
Fill="Red" />
<Rectangle x:Name="Rect2"
Canvas.Left="0"
Canvas.Top="600"
Width="100"
Height="100"
Fill="Green" />
<ListBox Canvas.Left="150"
Width="200"
Height="{Binding Path=ActualHeight, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollViewer}}}"
ItemsSource="{Binding YourDataSource}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Standard">
<ListBox.ItemTemplate>
<DataTemplate>
<Canvas>
<TextBlock Canvas.Left="{Binding RectangleLeft}"
Canvas.Top="{Binding RectangleTop}"
FontSize="20"
Text="{Binding Text}" />
</Canvas>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Canvas>
</ScrollViewer>
</Window>
code-behind:
using System.Collections.ObjectModel;
using System.Windows;
namespace ListBoxVirtualizationExample
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
YourDataSource = new ObservableCollection<DataItem>();
for (var i = 0; i < 200; i++)
{
YourDataSource.Add(new DataItem {Text = $"Item {i}", RectangleLeft = 0, RectangleTop = 20 * i});
}
DataContext = this;
}
public ObservableCollection<DataItem> YourDataSource { get; set; }
}
public class DataItem
{
public string Text { get; set; }
public double RectangleLeft { get; set; }
public double RectangleTop { get; set; }
}
}
This gives me results like this when scrolled down:
But ideally, it should look something similar to this (this picture is taken when the ListBox takes the entire height of the canvas, so basically no virtualization is on) :
That is, items 25-28 should always be in the green square no matter how the user changes the viewport and scrolls up and down.
The question is: how can I improve the performance of the UserControl? Is the ListBox control the right way to do this? Are there any other ways to achieve the same effect?
The virtualization is working. Because you wrap each item into a Canvas and apply the content position absolute, you are effectively moving the TextBlock elements out of the item container: each TextBlock is rendered on the screen relative to the Canvas (the one inside the DataTemplate).
Because you assigning them a Canvas.Top of a multiple of 20 the items are stacked by force and not by the VirtualizingStackPanel that the ListBox uses as panel.
If your idea was to add a top margin of 20 to each item, you must do this from the ListBox.ItemContainerStyle:
<ListBox>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Margin" Value="0,20,0,0" />
</Style>
<ListBox.ItemContainerStyle>
<ListBox>
If your idea is to place the TextBlock elements on a Canvas to position them by the location provided by the data item (and by stacking them inside the item panel), then instead of wrapping each item's content into a Canvas you must replace the panel of the ListBox with a Canvas and set Canvas.Top and Canvas.Left on the ListBoxItem (from the ItemContainerStyle). But unless you have acustom Canvas panel that supports virtualization you lose this performance feature.
The second issue is that your ListBox is obviously the wrong size. You give it the size of the ScrollViewer which is basically the size of the viewport.
If the ListBox is supposed to stretch across the Canvas you must bind it accordingly. Bind ListBox.Height to Canvas.Height.
The following code has removed the Canvas from the DataTemplate and fixed the binding set on the ListBox.Height property to make it behave as expected.
<ScrollViewer HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Visible">
<Canvas Width="2000"
Height="2000">
<Rectangle x:Name="Rect1"
Canvas.Left="0"
Canvas.Top="0"
Width="100"
Height="100"
Fill="Red" />
<Rectangle x:Name="Rect2"
Canvas.Left="0"
Canvas.Top="600"
Width="100"
Height="100"
Fill="Green" />
<ListBox Canvas.Left="150"
Width="200"
Height="{Binding Path=Height, RelativeSource={RelativeSource AncestorType={x:Type Canvas}}}"
ItemsSource="{Binding YourDataSource}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock FontSize="20"
Text="{Binding Text}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Canvas>
</ScrollViewer>
The first thing to try is give each item in that listbox a specific size.
Your listbox isn't virtualising because it doesn't have a size for each of it's items. They are all as big as they like because each item is going in a virtualising stackpanel and has nothing sets or constrains it's size.
If you give each item a size then the virtualising stackpanel can decide how many of them should fit and which will not.
That will of course virtualise those that do not fit.
You could do that with the canvas.
The height here is just arbritrary. You should pick whatever suits size of your rectangles. Maybe that's 50 * 1900. It's the height that matters, obviously.
<ListBox.ItemTemplate>
<DataTemplate>
** <Canvas Height="22" Width="200"> **
<TextBlock Canvas.Left="{Binding RectangleLeft}"
Canvas.Top="{Binding RectangleTop}"
FontSize="20"
Text="{Binding Text}" />
</Canvas>
</DataTemplate>
</ListBox.ItemTemplate>
I don't really follow why you have a canvas in each of the items though. It doesn't make much sense without an explanation. But maybe that doesn't matter so much.
If you can get the ui items down to around 10,000 then I think you'll likely find your problems greatly reduced.
If not then you could consider simplifying the controls in each item somehow. Difficult to say exactly what would be best without knowing more about the intent of this design. A drawingvisual encapsulating everything in an item into one thing might be something to consider.
I don't really follow why the whole thing is not one listbox with a canvas as itemspaneltemplate and everything rendered from that. You would just have the one scrollviewer to deal with.
With this scenario the 2000 x 2000 canvas would be the panel in the listbox and everything would be rendered in that.
It would virtualise ( if you tell it how big things are ).

Lock zIndex of a ContentPresenter Upon Mouse Enter or Drag Complete

I need to lock the Z-order of a canvas/content control after it is dragged by a Thumb.
In the below image, the "Joe Smith" pops above the others the other two ellipses while the the mouse over is active. Once the drag stops and the mouse moves out, it drops back to its value.
I am unable to find a solution within the design I have shown below to keep it locked above the others.
Minimal Reproducible Example
I have created a code gist of all code that contains the xaml, the thumb class and the people class. All one has to do is create a .Net Core WPF app and drop in the snippets of code and set name spacing in Xaml as needed.
Design
There is an ItemsControl which has DataTemplate defined with a Canvas. Each content in the ItemsControl has ContentControl which has a Thumb as applied by a style.
Another style trigger of the mouse entering the ContentPresenter has the content temporarily pushed in zIndex above the others by setting the content's internal Grid's zIndex to 1.
How to stick that zIndex?
Xaml
<ItemsControl Margin="10" ItemsSource="{Binding Source={StaticResource People}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<ContentControl Width="100" Height="100" >
<Grid>
<Ellipse Fill="Silver">
<Ellipse.Effect>
<DropShadowEffect Color="Black" Direction="320" ShadowDepth="6" Opacity="0.5"/>
</Ellipse.Effect>
</Ellipse>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Margin="3,3,3,0" Text="{Binding Path=First}"/>
<TextBlock Margin="3,0,3,7" Text="{Binding Path=Last}"/>
</StackPanel>
</Grid>
</ContentControl>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Grid.ZIndex" Value="1"/>
</Trigger>
</Style.Triggers>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
See the Gist for all supporting classes and styles to reproduce
Actual Design
The ultimate goal is to have a panel of images of a set size, when the user grabs the thumb the image it will move forward and lock above the others. I say this in-case there is another way to do that which could provide an answer above the minimal example design.
In my ItemsControl I changed the ItemsPanelTemplate to be a Canvas such as
<ItemsPanelTemplate>
<Canvas x:Name="MainCanvas" />
</ItemsPanelTemplate>
Which when looking at the Visual Tree where the user was clicking the ContentControl, it had a parent of a Canvas that had a ContentPresenter with that top level Canvas such as (see named MainCanvas):
I changed the ContentControl to have a MouseEnter event :
<ContentControl Width="100" Height="100" MouseEnter="EnterMouse">
<Grid>
<Ellipse Fill="Silver">
In that method I needed to find the named "MainCanvas" Canvas and enumerate all of its children the ContentPresenters and extract the max ZIndex and then set my ContentControl (shown in blue above) to that ZIndex value plus 1.
Here is the code behind where extract the necessary the parents:
private void EnterMouse(object sender, MouseEventArgs e)
{
if (sender is ContentControl cc)
{
var cpParent = FindParent<ContentPresenter>(cc);
var p2 = FindParent<Canvas>(cpParent);
var max = p2.Children.Cast<UIElement>().Max(control => Panel.GetZIndex(control));
Panel.SetZIndex(cpParent, max + 1);
}
}
private T FindParent<T>(DependencyObject child) where T : DependencyObject
{
DependencyObject immediateParent = VisualTreeHelper.GetParent(child);
T parent = immediateParent as T;
return parent ?? FindParent<T>(immediateParent);
}

UWP: Highlight ListBoxItem - Events That Coincide with the Highlighting

I have a ListBox bound to a collection of items of type Definition. My requirement is that every time the mouse is hovered over the area of a templated ListBoxItem, a second ListBox open next to the ListBoxItem, revealing sub-items which are of type Word.
(I am basically implementing something similar to a TreeView using two ListBoxes. This is for earlier versions so using a TreeView control is not an option.)
This is the data structure...
public class Word
{
public string Name { get; set; }
}
public class Definition
{
public string Name { get; set }
public ObservableCollection<Word> Words;
}
public class Dictionary
{
public string Name { get; set }
public ObservableCollection<Definition> Definitions;
}
And here is the XAML view...
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Button Grid.Row="0"
Height="0">
<Button.Flyout>
<Flyout x:Name="DefinitionFlyout"/>
<ListBox x:Name="WordsListBox" HorizontalAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemp late x:DataType="local:Word">
<TextBox TextWrapping="NoWrap"
Height="Auto"
BorderThickness="0"
HorizontalAlignment="Stretch"
Text="{x:Bind Name}"
TextAlignment="Left"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Flyout>
</Button.Flyout>
</Button>
<ListBox x:Name="DefinitionsListBox"
Grid.Row="1"
SelectionMode="Single"
HorizontalAlignment="Stretch">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="local:Definition">
<TextBox TextWrapping="NoWrap"
Height="Auto"
BorderThickness="0"
HorizontalAlignment="Stretch"
Text="{x:Bind Name, Mode=TwoWay}">
</TextBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
When the mouse pointer hovers over a Definition item in DefinitionsListBox, WordsListBox should fly out and display the Words of that Definition. And when the pointer exits that Definition and hovers over a new one, I want WordsListBox to reflect that change.
Unfortunately, I can't find the events that will help me accomplish this.
I thought defining PointerEntered and PointerExited in the TextBox of Definition would do the trick but they don't because PointerExited fires IMMEDIATELY after PointerEntered, as in almost simultaneously, and not when the mouse exits the TextBox area. And SelectionChanged of ListBox doesn't fire.
The first event should fire when ListBoxItem highlighting begins, and the second one, when the highlighting ends.
What do you recommend for this, please?
I thought defining PointerEntered and PointerExited in the TextBox of Definition would do the trick but they don't because PointerExited fires IMMEDIATELY after PointerEntered, as in almost simultaneously.
The problem is the when Flyout show at the button there is a mask layer cover on the window. This will prevent basic input event of TextBox defined. It looks PointerExited fires immediately after PointerEntered as in almost simultaneously.
For solving this , you could set OverlayInputPassThroughElement property for Flyout that make the area of ListBox could response PointerEntered PointerExited event when Flyout is opened. For more please refer the following code.
<Flyout x:Name="DefinitionFlyout" OverlayInputPassThroughElement="{x:Bind DefinitionsListBox}">

Settings Window design - binding Page to TreeViewItem

I'm just starting with WPF (coming from WinForms) and I want to make settings window like the one in Visual Studio. Category tree on left side and appropriate views on the right.
In WinForms I did it by adding to TreeViewItem's TAG name of the window that should be displayed and then in OnClick I was creating that window using reflection. Something like this:
//pseudocode
TreeViewItem item = CreateTreeViewItem();
item.Tag = "GeneralSettingsWindow";
item.Text = "General settings";
------------------------------------------------------------------------
void ItemClick(object sender)
{
TreeViewItem item = sender as TreeViewItem;
string formName = item.Tag.ToString();
BaseSettingsForm f = Activator.CreateInstance(formName);
settingsPanel.Controls.Clear();
settingsPanel.Controls.Add(f);
}
And it worked fine.
I'm curious how can I achieve that functionality using WPF binding. Is it possible anyway?
I have done a tree view in left side and when you click an item it will update the contents of right side panel. In order to do that you have to make two panels or grid in an window, one in left side with less width and another in right side. Then put the tree view in the left side and update the contents of right side grid by adding different types of user controls or by showing and hiding different grids with the corresponding click of tree view item.
Here is an example of making the tree view and you can modify that according to your own requirement.
<TreeView MouseLeftButtonUp="Get_MFR" SelectedItemChanged="treeviewQuestion_SelectedItemChanged"
x:Name="treeviewQuestion" FontFamily="Nunito" FontSize="16" Padding="10 10"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
BorderThickness="0" Margin="0,0,0,27"
>
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="True"/>
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="10 2">
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}},Path=Tag,
Converter={x:Static local:TagToTextConverter.Instance}}" />
<TextBlock Text="{Binding}"/>
<Rectangle Margin="5" Width="10" Height="10" Fill="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type TreeViewItem}},Path=Tag,
Converter={x:Static local:TagToColorConverter.Instance}}"/>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
</TreeView>
Even though you're new to WPF development, I'd still suggest you take a look at the MVVM design pattern. The separation between user interface and business logic makes this type of application structure much more straightforward.
Replace your settings panel with a ContentControl, and define as many DataTemplates as you require for different tree node types. You can then bind its Content property to the selected TreeViewItem.
Unfortunately, "out of the box" WPF doesn't support binding to the selected item in a TreeView. However, there are several ways around that, including using a Behaviour, which I described in a recent blog post.

WPF Custom Control Work by itself but not as part of another control

I have a WPF Custom Control that I use to display images.
There is a list view which is bound to an observable collection of database entities, which is subsequently converted into an image by virtue of a value converter.
When I drag the control onto a windows forms project (using a WPF Host control) it works perfectly when assigning the observable collection behind a list. I have tested this and it updates correctly and does everything i need it to.
HOWEVER
I would like to have three such controls displaying related images so I created a second control which simply grouped the three original controls into a stack panel.
I created a method for each that updates the images property.
public void ChangeSearchResults(List<ItemImage> items)
{
SearchResultsImageViewer.ItemImages = new ObservableCollection<ItemImage>(items);
}
However I simply cant get the images to show.
There seems to be a difference between viewing a control directly and viewing a control as a child control.
I am pretty sure it is not the element host in winforms as the control works well by itself.
Is there something I am not realising?
This is the Xaml for the list view
<!-- Sets the template for the data to be displayed -->
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<!-- Defines the actual image being displayed -->
<Image x:Name="ItemImageControl"
Width="100"
Height="200"
Margin="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Cursor="Hand"
Source="{Binding .,
Converter={StaticResource imageConverter}}" />
<TextBlock HorizontalAlignment="Center"
FontWeight="Bold"
Text="{Binding .,
Converter={StaticResource groupNameConverter}}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
EDIT - this is the user control XAML
<UserControl x:Class="Project.CustomControls.ctrlImageCollection"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:CustomControls="clr-namespace:Project.CustomControls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<StackPanel>
<CustomControls:ctrlImageViewer x:Name="ShortlistImageViewer" />
<CustomControls:ctrlImageViewer x:Name="SearchResultsImageViewer" />
<CustomControls:ctrlImageViewer x:Name="GroupImageViewer" />
</StackPanel>
</UserControl>
With using
...
Source={Binding ., Converter={StaticResource imageConverter}}" />
you bind to the current UserControl. When you use your ListView alone, it is bound to the UserControl containing the ListView and will work.
If you put your ListView UserControl into another UserControl its values get bound to its parent UserControl, not longer "own" UserControl.
Try this:
Go to your ListView UserControl xaml.cs and set the DataContext to itself.
//...
DataContext = this;
//...

Categories