|
The idea with a collection that implements INotifyCollectionChanged , is that it will be (possibly) be used in an ItemsControl , which will get the CollectionViewSource.GetDefaultView(Object) for it, that is likely to be a ListCollectionView .
So far... so good...
However, as I just discovered today , when using AddRange() , RemoveRange() , i.e. creating Add/Remove events with more that 1 object will trigger the following error in the ListCollectionView :
System.NotSupportedException: Range actions are not supported.
Stack Trace:
ListCollectionView.ValidateCollectionChangedEventArgs(NotifyCollectionChangedEventArgs e)
ListCollectionView.ProcessCollectionChanged(NotifyCollectionChangedEventArgs args)
AbstractInvocation.Proceed()
Invocation.CallBase() line 147
ReturnBaseOrDefaultValue.Execute(Invocation invocation) line 88
IInterceptor.Intercept(Invocation invocation) line 34
Interceptor.Intercept(IInvocation underlying) line 113
AbstractInvocation.Proceed()
NotifyCollectionChangedEventHandler.Invoke(Object sender, NotifyCollectionChangedEventArgs e)
Mmm... unfortunate...
I can think of 2 possible fixes and I am undecided as to which one to use?!
- When a range
Add/Remove event if fired, instead fire a Reset event. - When a range
Add/Remove event if fired, instead fire multiple Add/Remove single item events. - or.. mmm... have a FireEventMode property, which would let the user decide whether to use event as is, split it, or fire Reset.
Wo.. what say you?
|
|
|
|
|
|
I have a DataGrid where we display an animation on DataGridRows when a property in the ItemSource is set.
If the height of the DataGrid is enough low to display the VerticalScrollBar, I can scroll up and down and see the animations jump to the wrong rows.
I have made code snippet that reproduce the issue:
* The code shuld be "copy-paste".
* The DataGrid displays a list of Persons.
* If a person's HasBirthday = true the corresponding DataGridRow displays the animation.
* The first item in the list has HasBirthday = true .
To reproduce
1) Notice the first row has the animation running already.
2) Click the second row in the Grid.
3) Scroll down.
3) Some other row should now also have the animation on it.
Scrolling up and down a couple of times should also diplay the issue.
* Any explanation why this happens?
* Any suggestions how to fix this?
/BR
Steffe
XAML
<Window x:Class="Main.Views.DataGridCustomAnimation"
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:Main.Views"
mc:Ignorable="d"
Loaded="Window_Loaded"
Title="DataGridCustomAnimation" Height="450" Width="800">
<Window.Resources>
<Style TargetType="{x:Type DataGridRow}" x:Key="DataGridRowSmallStyle">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="Background" Value="LightGray" />
<Setter Property="MinHeight" Value="26" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Gray" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="20" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DataGrid x:Name="grid"
Grid.Row="0"
AutoGenerateColumns="True">
<DataGrid.Resources>
<Style BasedOn="{StaticResource DataGridRowSmallStyle}" TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding HasBirthday}" Value="True" />
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="False" />
<Condition Binding="{Binding IsSelected, RelativeSource={RelativeSource Self}}" Value="False" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(DataGridRow.Background).(SolidColorBrush.Color)">
<EasingColorKeyFrame KeyTime="0:0:0.0" Value="#ca0516"/>
<EasingColorKeyFrame KeyTime="0:0:1.0" Value="#ca0516"/>
<EasingColorKeyFrame KeyTime="0:0:2.0" Value="#db552c"/>
<EasingColorKeyFrame KeyTime="0:0:3.0" Value="#ca0516"/>
</ColorAnimationUsingKeyFrames>
<!-- Note: -->
<!-- Cannot set the Foreground to white with a 'setter' when using an animation. -->
<ColorAnimation Storyboard.TargetProperty="(DataGridRow.Foreground).(SolidColorBrush.Color)"
From="White"
To="White" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.EnterActions>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
</DataGrid>
<Button Grid.Row="2" Height="40" Width="40" Content="Test" Click="Test_Clicked" />
</Grid>
</Window> C# code behind
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Main.Views
{
public partial class DataGridCustomAnimation : Window
{
public DataGridCustomAnimation()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var list = new List<Person>();
for (int i = 0; i < 40; i++)
{
list.Add(new Person
{
Name = $"Person {i}"
});
}
list.First().HasBirthday = true;
grid.ItemsSource = list;
}
private void Test_Clicked(object sender, RoutedEventArgs e)
{
var hasBirthdays = (grid.ItemsSource as List<Person>).Where(x => x.HasBirthday);
Console.WriteLine(hasBirthdays.Count());
}
}
public class Person
{
public string Name { get; set; }
public bool HasBirthday { get; set; }
public bool IsDead { get; set; }
}
}
|
|
|
|
|
I would just change the color of the row. Cost versus benefit.
"Before entering on an understanding, I have meditated for a long time, and have foreseen what might happen. It is not genius which reveals to me suddenly, secretly, what I have to say or to do in a circumstance unexpected by other people; it is reflection, it is meditation." - Napoleon I
|
|
|
|
|
Hi!
Sorry I don't understand.
Can you please explain what you mean?
|
|
|
|
|
I am trying to print labels with a height of 38mm. The number in a run can be from 1 to 2000. The preview like in a document viewer would be desirable. The label height is entered in the printer's print settings. The printer is not the default printer in the user profile. In the DocumentViewer, however, the print settings are only loaded if it is the default printer.
I want to specify the printer from a CONFIG file and the label height must be 38mm, not DIN A4!
I can't get any further without a program example. Thank you for help.
|
|
|
|
|
|
Hey all I have the following OnStartup for my WPF desktop app that goes to a local directory and gathers all the images within that folder (box1 being the example code below):
<pre lang="C#">public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var imageSource = new ImageSource(Path.Combine(@"C:\photos\boxes\", "box1"), TimeSpan.FromHours(1));
var viewModel = new MainWindowViewModel(imageSource);
var window = new MainWindow()
{
DataContext = viewModel
};
window.Closed += (s, a) => { viewModel.Dispose(); };
window.Show();
}
}
This works just fine for the component I have on the MainWindow.xaml that's label box1 but the other boxes 2-9 do not load their own images from their respective folders - they all show the same images as box1 has.
The structure of the directory is this:
C:\
|-photos\
|boxes\
|box1
|box2
|box3
|box4
|box5
|box6
|box7
|box8
|box9
|box10
On the MainWindow I have this code that allows all those photos from each directory into its own element:
<Window.Resources>
<FluidKit:SlideTransition x:Key="SlideTransition" x:Shared="False"/>
<FluidKit:CubeTransition x:Key="CubeTransition" Rotation="BottomToTop" x:Shared="False"/>
<FluidKit:FlipTransition x:Key="FlipTransition" x:Shared="False"/>
<local:ImageSourceConverter x:Key="ImageSourceConverter"/>
<DataTemplate x:Key="ItemTemplate" x:Shared="False">
<Image Source="{Binding Path, Converter={StaticResource ImageSourceConverter}}"
Stretch="Fill"/>
</DataTemplate>
</Window.Resources>
<Grid>
<FluidKit:TransitionPresenter RestDuration="0:0:3"
IsLooped="True"
Transition="{StaticResource FlipTransition}"
ItemsSource="{Binding box1}"
Width="357"
Height="272"
HorizontalAlignment="Left"
VerticalAlignment="Top"
ItemTemplate="{StaticResource ItemTemplate}"
x:Name="box1"
Margin="0,0,0,454"/>
<FluidKit:TransitionPresenter RestDuration="0:0:3"
IsLooped="True"
Transition="{StaticResource FlipTransition}"
ItemsSource="{Binding box2}"
Width="357"
Height="272"
HorizontalAlignment="Left"
VerticalAlignment="Top"
ItemTemplate="{StaticResource ItemTemplate}"
x:Name="box2"
Margin="357,0,0,0"/>
ETC.......
Like I said above, this first element ItemsSource="{Binding box1} loads the images as they should be from the box1 directory just fine but all the other 2-9 after that are loaded the same images as box1.
The mainWindowViewModel code looks like this:
public class MainWindowViewModel : IDisposable
{
private readonly IDisposable _token1;
private readonly IDisposable _token2;
...ETC
public MainWindowViewModel(IImageSource imageSource)
{
_token1 = imageSource
.GetImages().ObserveOn(DispatcherScheduler.Current)
.Subscribe(i => UpdateImages(i, box1), ex => ShowError(ex));
_token2 = imageSource
.GetImages().ObserveOn(DispatcherScheduler.Current)
.Subscribe(i => UpdateImages(i, box2), ex => ShowError(ex));
ETC...
}
private void ShowError(Exception ex)
{
MessageBox.Show(ex.Message, "Photo Gallery", MessageBoxButton.OK, MessageBoxImage.Error);
}
private void UpdateImages(IEnumerable<string> images, ObservableCollection<ImageViewModel> animation)
{
animation.Clear();
foreach (var i in images)
{
animation.Add(new ImageViewModel { Path = i });
}
}
public ObservableCollection<ImageViewModel> box1 { get; set; } =
new ObservableCollection<ImageViewModel>();
public ObservableCollection<ImageViewModel> box2 { get; set; } =
new ObservableCollection<ImageViewModel>();
ETC...
public void Dispose()
{
_token1.Dispose();
_token2.Dispose();
ETC...
}
}
The function that loops to get each file image within the directory is this:
public class ImageSource : IImageSource
{
private readonly string _path;
private readonly TimeSpan _interval;
public ImageSource(string path, TimeSpan interval)
{
_path = path;
_interval = interval;
}
public IObservable<IEnumerable<string>> GetImages()
{
if (!Directory.Exists(_path))
{
return Observable.Empty<IEnumerable<string>>();
}
return Observable.Create<IEnumerable<string>>(observer =>
{
return TaskPoolScheduler.Default.ScheduleAsync(async (ctrl, ct) =>
{
for (;;)
{
if (ct.IsCancellationRequested)
{
break;
}
try
{
var images = Directory.GetFiles(_path, "*.jpg");
if (images.Count() > 9)
{
observer.OnNext(images.PickRandom());
}
}
catch (Exception ex)
{
observer.OnError(ex);
throw;
}
await ctrl.Sleep(_interval).ConfigureAwait(false);
}
});
});
}
The code currently goes to each of the ObservableCollection<imageviewmodel> box[X] { get; set; } and sets the path to each image within that folder. Box2-10 are the same files as box 1 of course.
How can I modify that onStartup() code to allow for it to consume each box folders' files and place them into the appropriate box # component instead of just using that box1 files?
Thanks!
|
|
|
|
|
You don't appear to be varying your input path. You need a list or a "box path counter".
var imageSource = new ImageSource(Path.Combine(@"C:\photos\boxes\", "box1"),
TimeSpan.FromHours(1));
"Before entering on an understanding, I have meditated for a long time, and have foreseen what might happen. It is not genius which reveals to me suddenly, secretly, what I have to say or to do in a circumstance unexpected by other people; it is reflection, it is meditation." - Napoleon I
|
|
|
|
|
No related to your question but.... you XAML format is absurd. That's not at all standard and will cause TAB issues.
If it's not broken, fix it until it is.
Everything makes sense in someone's mind.
Ya can't fix stupid.
|
|
|
|
|
Hey all it has been a few years since I last worked on my WPF code and sadly I'm not remembering what all I did in order for it to work as its coded.
The current issue I am facing is this:
An object of the type 'System.Windows.Media.ImageSourcesConverter' cannot be applied to a property that expects the type 'System.Windows.Data.IValueConverter'.
It has the above error on this line of code:
<Image Source="{Binding Path,
Converter={StaticResource ImageSourceConverter}}"
Stretch="Fill"
/>
The full XAML code being:
<Window.Resources>
<!-- List of supported animations -->
<FluidKit:SlideTransition x:Key="SlideTransition" x:Shared="False"/>
<FluidKit:CubeTransition x:Key="CubeTransition" Rotation="BottomToTop" x:Shared="False"/>
<FluidKit:FlipTransition x:Key="FlipTransition" x:Shared="False"/>
<local1:ImageSourceConverter x:Key="ImageSourceConverter"/>
<!-- Data template for animations -->
<DataTemplate x:Key="ItemTemplate" x:Shared="False">
<Image Source="{Binding Path,
Converter={StaticResource ImageSourceConverter}}"
Stretch="Fill"/>
</DataTemplate>
</Window.Resources>
<Grid x:Name="imageAreas" HorizontalAlignment="Left" Height="1920" Margin="0,0,0,0" VerticalAlignment="Top" Width="1080" ScrollViewer.VerticalScrollBarVisibility="Disabled">
<Grid HorizontalAlignment="Left" Height="726" VerticalAlignment="Top" Width="1080" x:Name="topGrid">
<FluidKit:TransitionPresenter RestDuration="0:0:3"
IsLooped="True"
Transition="{StaticResource FlipTransition}"
ItemsSource="{Binding Images1}"
Width="357"
Height="272"
HorizontalAlignment="Left"
VerticalAlignment="Top"
ItemTemplate="{StaticResource ItemTemplate}"
x:Name="box1"
Margin="0,0,0,454"/>
<FluidKit:TransitionPresenter RestDuration="0:0:3"
IsLooped="True"
Transition="{StaticResource FlipTransition}"
ItemsSource="{Binding Images2}"
Width="357"
Height="272"
HorizontalAlignment="Left"
VerticalAlignment="Top"
ItemTemplate="{StaticResource ItemTemplate}"
x:Name="box2"
Margin="357,0,0,0"/>
....ETC....
It is references what I believe is this class (ImageSourceConverter.cs):
public sealed class ImageSourceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string path = value as string;
if (path != null)
{
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = new FileStream(path, FileMode.Open, FileAccess.Read);
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.StreamSource.Dispose();
return bitmapImage;
}
else
{
return DependencyProperty.UnsetValue;
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return DependencyProperty.UnsetValue;
}
}
My View Model (MainWindowViewModel.cs) is this:
public class MainWindowViewModel : IDisposable
{
private readonly IDisposable _token1;
private readonly IDisposable _token2;
private readonly IDisposable _token3;
private readonly IDisposable _token4;
private readonly IDisposable _token5;
private readonly IDisposable _token6;
private readonly IDisposable _token7;
private readonly IDisposable _token8;
private readonly IDisposable _token9;
private readonly IDisposable _token10;
public MainWindowViewModel(IImageSource imageSource)
{
_token1 = imageSource
.GetImages(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "box1"))
.ObserveOn(DispatcherScheduler.Current)
.Subscribe(i => UpdateImages(i, box1), ex => ShowError(ex));
_token2 = imageSource
.GetImages(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "box2"))
.ObserveOn(DispatcherScheduler.Current)
.Subscribe(i => UpdateImages(i, box2), ex => ShowError(ex));
_token3 = imageSource
.GetImages(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "box3"))
.ObserveOn(DispatcherScheduler.Current)
.Subscribe(i => UpdateImages(i, box3), ex => ShowError(ex));
_token4 = imageSource
.GetImages(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "box4"))
.ObserveOn(DispatcherScheduler.Current)
.Subscribe(i => UpdateImages(i, box4), ex => ShowError(ex));
_token5 = imageSource
.GetImages(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "box5"))
.ObserveOn(DispatcherScheduler.Current)
.Subscribe(i => UpdateImages(i, box5), ex => ShowError(ex));
_token6 = imageSource
.GetImages(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "box6"))
.ObserveOn(DispatcherScheduler.Current)
.Subscribe(i => UpdateImages(i, box6), ex => ShowError(ex));
_token7 = imageSource
.GetImages(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "box7"))
.ObserveOn(DispatcherScheduler.Current)
.Subscribe(i => UpdateImages(i, box7), ex => ShowError(ex));
_token8 = imageSource
.GetImages(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "box8"))
.ObserveOn(DispatcherScheduler.Current)
.Subscribe(i => UpdateImages(i, box8), ex => ShowError(ex));
_token9 = imageSource
.GetImages(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "box9"))
.ObserveOn(DispatcherScheduler.Current)
.Subscribe(i => UpdateImages(i, box9), ex => ShowError(ex));
_token10 = imageSource
.GetImages(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "box10"))
.ObserveOn(DispatcherScheduler.Current)
.Subscribe(i => UpdateImages(i, box10), ex => ShowError(ex));
}
private void ShowError(Exception ex)
{
MessageBox.Show(ex.Message,
"Photo Gallery", MessageBoxButton.OK, MessageBoxImage.Error);
}
private void UpdateImages(IEnumerable<string> images, ObservableCollection<ImageViewModel> animation)
{
animation.Clear();
foreach (var i in images)
{
animation.Add(new ImageViewModel { Path = i });
}
}
public ObservableCollection<ImageViewModel> box1 { get; set; } =
new ObservableCollection<ImageViewModel>();
public ObservableCollection<ImageViewModel> box2 { get; set; } =
new ObservableCollection<ImageViewModel>();
public ObservableCollection<ImageViewModel> box3 { get; set; } =
new ObservableCollection<ImageViewModel>();
public ObservableCollection<ImageViewModel> box4 { get; set; } =
new ObservableCollection<ImageViewModel>();
public ObservableCollection<ImageViewModel> box5 { get; set; } =
new ObservableCollection<ImageViewModel>();
public ObservableCollection<ImageViewModel> box6 { get; set; } =
new ObservableCollection<ImageViewModel>();
public ObservableCollection<ImageViewModel> box7 { get; set; } =
new ObservableCollection<ImageViewModel>();
public ObservableCollection<ImageViewModel> box8 { get; set; } =
new ObservableCollection<ImageViewModel>();
public ObservableCollection<ImageViewModel> box9 { get; set; } =
new ObservableCollection<ImageViewModel>();
public ObservableCollection<ImageViewModel> box10 { get; set; } =
new ObservableCollection<ImageViewModel>();
public void Dispose()
{
_token1.Dispose();
_token2.Dispose();
_token3.Dispose();
_token4.Dispose();
_token5.Dispose();
_token6.Dispose();
_token7.Dispose();
_token8.Dispose();
_token9.Dispose();
_token10.Dispose();
}
}
The IImageSource.cs:
public interface IImageSource
{
IObservable<IEnumerable<string>> GetImages(string path);
}
And lastly the ImageSource.cs:
public class ImageSource : IImageSource
{
TimeSpan _pollingInterval;
public ImageSource(TimeSpan pollingInterval)
{
_pollingInterval = pollingInterval;
}
public IObservable<IEnumerable<string>> GetImages(string path)
{
if (!Directory.Exists(path))
{
return Observable.Empty<IEnumerable<string>>();
}
try
{
return Observable.Create<IEnumerable<string>>(observer =>
{
return TaskPoolScheduler.Default.ScheduleAsync(async (ctrl, ct) =>
{
for (; ; )
{
if (ct.IsCancellationRequested)
{
break;
}
try
{
var images = Directory.GetFiles(path, "*.jpg");
observer.OnNext(images);
}
catch (Exception ex)
{
observer.OnError(ex);
throw;
}
await ctrl.Sleep(_pollingInterval).ConfigureAwait(false);
}
});
});
} catch (Exception ex)
{
Console.WriteLine(ex.Message);
return null;
}
}
}
From what I can remember, It loads up the XAML page there and populates each box with images that correspond to a directory called c:\images\box1...10. But as the error at the top of this says I’m not able to do that even though the program still runs just fine - be it without the images of course.
Would be great if someone could tell me what I was up too ha back those many years.
|
|
|
|
|
The error message refers to the System.Windows.Media.ImageSourcesConverter class. Even if your custom ImageSourceConverter was in that namespace, the name doesn't match - ImageSources is not the same as ImageSource (without the "s").
Unless that's a typo in your question, then either the error message is referring to something other than the code you've posted, or the code you've posted is not the code that's running.
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
|
|
|
|
|
|
So as I said, either there's a typo in your question, or the code you've shown isn't the code that's generating that error.
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
|
|
|
|
|
I have a TextBlock in a WPF page that will be displayed after an action, it can be a "green" message or a "red" message if an action is successful or not.
Currently, I have binding for the color (I also have a property for the text content and the visibility). : (simplified)
public string MessageColor
{
get
{
if ( someBooleanValue )
return "green";
else
return "red";
}
}
public string Message
{
get
{
if ( someBooleanValue )
return "Success Message";
else
return "Failure Message";
}
}
This seems awkward or clumsy.
Would it be better to have 2 TextBlock in the XAML with their specific styles and toggle the visibility ?
CI/CD = Continuous Impediment/Continuous Despair
|
|
|
|
|
You could use a style with triggers to change the TextBlock properties when the view-model property changes:
public bool SomeBooleanValue
{
get { return _someBooleanValue; }
private set { SetProperty(ref _someBooleanValue, value); }
}
<TextBlock>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=SomeBooleanValue}" Value="True">
<Setter Property="Foreground" Value="Green" />
<Setter Property="Text" Value="Success Message" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=SomeBooleanValue}" Value="False">
<Setter Property="Foreground" Value="Red" />
<Setter Property="Text" Value="Failure Message" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock> Trigger, DataTrigger & EventTrigger - The complete WPF tutorial[^]
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
|
|
|
|
|
Thanks, looking at it.
What is the "SetProperty" ? is this something I have to implement ?
Scratch that, it's working.
Thanks.
CI/CD = Continuous Impediment/Continuous Despair
modified 26-Jan-22 15:39pm.
|
|
|
|
|
Hi Geeks !
I bang my head on what looked to be a ridiculously trivial problem...
In a sentence: I need to display a image which is in a form of byte[] (array of byte) in a WPF ItemsControl.
'Sounds simple enough right ? I could find tens of solutions on the web, all similarly making use of a MemoryStream assigned to an BitmapImage. See snippet bellow in C# 10:
public static ImageSource? BitmapFromRaw(byte[]? imageData)
{
if (imageData == null) return null;
var image = new BitmapImage();
var mem = new MemoryStream(imageData);
image.BeginInit();
image.StreamSource = mem;
image.EndInit();
image.Freeze();
return image;
}
At first it looks like it works fine. The images converted this way could be displayed in an Image WPF control. BUT: this leads to a big memory leak. Looking at the memory usage in Visual Studio it shows that neither the BitmapImage nor the MemoryStream get freed from memory. From the lines commented out, you could see I've already tried to tweak this with different options... no success.
Here is the smallest code I could make to reproduce the problem:
MainWindow.xaml
<Window x:Class="Toy.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:Toy"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<StackPanel>
<Button Content="Collect Garbage" Click="gcButton_Click"/>
<Button x:Name="wxButton" Click="wxButton_Click"/>
<ItemsControl x:Name="wxToy" ItemsSource="{Binding}" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Bisque" Width="100" Height="100" BorderThickness="2">
<Image Source="{Binding Data}"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Toy
{
internal class ToyItem : INotifyPropertyChanged
{
#region Boilerplate INotifyPropertyChanged
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChanged?.Invoke(this, e);
}
public void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler? PropertyChanged;
#endregion
public static ImageSource? BitmapFromRaw(byte[]? imageData)
{
if (imageData == null) return null;
var image = new BitmapImage();
var mem = new MemoryStream(imageData);
image.BeginInit();
image.StreamSource = mem;
image.EndInit();
image.Freeze();
return image;
}
public ImageSource? Data
{
get { return _Data; }
set
{
if (value != _Data)
{
_Data = value;
OnPropertyChanged();
}
}
}
private ImageSource? _Data;
public ToyItem ()
{
Data = BitmapFromRaw(Properties.Resources.pexels_jonathan_faria_8581946);
}
~ToyItem()
{
}
}
public partial class MainWindow : Window
{
ObservableCollection<ToyItem> ToyList = new ObservableCollection<ToyItem>();
int Counter = 0;
public MainWindow()
{
InitializeComponent();
wxToy.DataContext = ToyList;
wxButton.Content = Counter;
}
private void gcButton_Click(object sender, RoutedEventArgs e)
{
GC.Collect();
}
private void wxButton_Click(object sender, RoutedEventArgs e)
{
wxButton.Content = ++Counter;
ToyList.Clear();
for (int i = 0; i < 5; i++)
{
ToyList.Add(new ToyItem());
}
}
}
}
You can download a ready-to-use solution of this here
This example uses a resource (Properties.Resources.pexels_jonathan_faria_8581946) to get an image in byte[]. This is only for simplifying the example, don't bother to explain how I could workaround the problem by using a jpg image and URI instead. In the real application, I have no choice but getting the images from byte[] arrays...
If you run this code, you could hit a button to generate and show 5 images at a time in a MVVM scheme in an ItemsControl, just the way we are suppose to do it in WPF/C# I guess... Clicking the button several times will bring this deadly simple code to progressively eat Gigabytes of memory.
Please help me ! What am I doing wrong ?!
EDIT: Problem has been solved
I eventually posted a bug report to the WPF GitHub: Displaying an Image from a byte[] in an ItemControl leads to huge memory leak.
They kindly indicated that this was a known problem referenced in Known issus: WPF Image memory leak when remove image from visual tree.
There is actually a workaround to that problem, but nothing you could do yet to solve this in a proper MVVM scheme.
In a few words, changing the code of wxButton_Click as below will fix the memory leak:
private void wxButton_Click(object sender, RoutedEventArgs e)
{
for (int i = 0; i < wxToy.Items.Count; i++)
{
var uiElement = (FrameworkElement)wxToy.ItemContainerGenerator.ContainerFromIndex(i);
uiElement.DataContext = null;
uiElement.UpdateLayout();
}
wxButton.Content = ++Counter;
ToyList.Clear();
for (int i = 0; i < 5; i++)
{
ToyList.Add(new ToyItem());
}
}
Full story is there: Displaying an Image from a byte[] in an ItemControl leads to huge memory leak.
Thanks to all the coders who replied to this post !
modified 10-Feb-22 1:42am.
|
|
|
|
|
You're not doing any "disposing"; that's why you have a memory leak.
You need to keep a reference to the memory stream you create, then in the next go around, you check if that reference is not null; if it isn't, you dispose it before creating another stream and running with that.
Your other dispose attempts were (probably) premature.
"Before entering on an understanding, I have meditated for a long time, and have foreseen what might happen. It is not genius which reveals to me suddenly, secretly, what I have to say or to do in a circumstance unexpected by other people; it is reflection, it is meditation." - Napoleon I
|
|
|
|
|
Thanks for your reply Gerry!
In view of your suggestion, I've added the following lines just before the ToyList.Clear() call:
foreach (var item in ToyList)
{
if (item.Data is BitmapImage img)
{
img.StreamSource.Dispose();
}
}
Unfortunatly, the problem remains: there is no freeing up of those MemoryStream...
|
|
|
|
|
Well, your byte[] is a managed resource; so you might try .Dispose( true ); otherwise, I would use the debugger to follow the stream after the Dispose.
protected override void Dispose( bool disposing );
"Before entering on an understanding, I have meditated for a long time, and have foreseen what might happen. It is not genius which reveals to me suddenly, secretly, what I have to say or to do in a circumstance unexpected by other people; it is reflection, it is meditation." - Napoleon I
|
|
|
|
|
As the Dispose(bool) is a protected method, I couldn't use it directly in this context. Moreover, the MemoryStream documentation indicates that calling Dispose() doesn't do anything and is not necessary...
Looks like it's going the wrong way...
Another test I did is using the Image without ItemsCollection, directly:
<Image Source="{Binding [0].Data}" Width="100" Height="100"/>
This show some errors on the output console, but the Image gets display and surprisingly the BitmapImage and MemoryStream objects get freed/disposed properly. So it really looks like the problem is dependent to the use of ItemsCollection...
|
|
|
|
|
Remove the finalizer from the ToyItem class. That class doesn't hold any unmanaged resources, so the only effect of adding a finalizer is to prolong the lifetime of the instance until the GC's finalizer thread runs.
If you want to have the stream disposed of when the item is removed, have ToyItem implement IDisposable :
internal sealed class ToyItem : INotifyPropertyChanged, IDisposable
{
...
public void Dispose()
{
if (Data is BitmapImage image)
{
image.StreamSource.Dispose();
}
Data = null;
}
} Unfortunately, the ObservableCollection<T> class doesn't dispose of items when they are removed. You can create a custom collection class to do that:
public class DisposableObservableCollection<T>
: ObservableCollection<T>, IDisposable
where T : IDisposable
{
public DisposableObservableCollection(List<T> list) : base(list)
{
}
public DisposableObservableCollection(IEnumerable<T> collection) : base(collection)
{
}
public DisposableObservableCollection()
{
}
protected override void SetItem(int index, T item)
{
T oldItem = this[index];
base.SetItem(index, item);
oldItem?.Dispose();
}
protected override void RemoveItem(int index)
{
T item = this[index];
base.RemoveItem(index);
item?.Dispose();
}
protected override void ClearItems()
{
List<T> itemsToDispose = Items.Where(i => i != null).ToList();
base.ClearItems();
foreach (T item in itemsToDispose)
{
item.Dispose();
}
}
} Then you can use that collection in your viewmodel:
ObservableCollection<ToyItem> ToyList = new DisposableObservableCollection<ToyItem>();
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
|
|
|
|
|
Hi Richard,
Thanks a lot for this very complete answer.
I've removed the finalizer and implemented the IDisposable as you suggested. I like the DisposableObservableCollection proposal. It looks very neat this way.
Unfortunatly this doesn't solve the problem. The leak is just as bad as before, with Image/BitmapImage and MemoryStream never being freed. It has probabably something to do with the fact that the MemoryStream.Dispose() doesn't do anything like explain on this page.
In the end the code looked like this:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Toy
{
internal class ToyItem : INotifyPropertyChanged, IDisposable
{
#region Boilerplate INotifyPropertyChanged
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChanged?.Invoke(this, e);
}
public void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler? PropertyChanged;
#endregion
public void Dispose()
{
if (Data is BitmapImage image)
{
image.StreamSource.Dispose();
}
Data = null;
}
public static ImageSource? BitmapFromRaw(byte[]? imageData)
{
if (imageData == null) return null;
var image = new BitmapImage();
var mem = new MemoryStream(imageData, false);
image.BeginInit();
image.StreamSource = mem;
image.EndInit();
image.Freeze();
return image;
}
public ImageSource? Data
{
get { return _Data; }
set
{
if (value != _Data)
{
_Data = value;
OnPropertyChanged();
}
}
}
private ImageSource? _Data;
public ToyItem ()
{
Data = BitmapFromRaw(Properties.Resources.pexels_jonathan_faria_8581946);
}
}
public class DisposableObservableCollection<T> : ObservableCollection<T>, IDisposable
where T : IDisposable
{
public DisposableObservableCollection(List<T> list) : base(list)
{
}
public DisposableObservableCollection(IEnumerable<T> collection) : base(collection)
{
}
public DisposableObservableCollection() : base ()
{
}
protected override void SetItem(int index, T item)
{
T oldItem = this[index];
base.SetItem(index, item);
oldItem?.Dispose();
}
protected override void RemoveItem(int index)
{
T item = this[index];
base.RemoveItem(index);
item?.Dispose();
}
protected override void ClearItems()
{
List<T> itemsToDispose = Items.Where(i => i != null).ToList();
base.ClearItems();
foreach (T item in itemsToDispose)
{
item.Dispose();
}
}
public void Dispose()
{
ClearItems();
}
}
public partial class MainWindow : Window
{
DisposableObservableCollection<ToyItem> ToyList = new DisposableObservableCollection<ToyItem>();
int Counter = 0;
public MainWindow()
{
InitializeComponent();
wxToy.DataContext = ToyList;
wxButton.Content = Counter;
}
private void gcButton_Click(object sender, RoutedEventArgs e)
{
GC.Collect();
}
private void wxButton_Click(object sender, RoutedEventArgs e)
{
wxButton.Content = ++Counter;
ToyList.Clear();
for (int i = 0; i < 5; i++)
{
ToyList.Add(new ToyItem());
}
}
}
}
|
|
|
|
|
I do a lot of image manipulation in UWP; but create an image from my "pixel buffer" instead of holding unto streams when I display it. I think you said you didn't want to use an image / jpg.
"Before entering on an understanding, I have meditated for a long time, and have foreseen what might happen. It is not genius which reveals to me suddenly, secretly, what I have to say or to do in a circumstance unexpected by other people; it is reflection, it is meditation." - Napoleon I
|
|
|
|
|