I'm creating a simple file browser with thumbnails. I'm using a ListBox with a custom DataTemplate to display objects in an ObservableCollection.
<DataTemplate>
<StackPanel Margin="5">
<Image Source="{Binding Path=ThumbnailPath}"/>
<Label Background="White" Content="{Binding Path=FileName}"/>
</StackPanel>
</DataTemplate>
The objects (of my custom class File) have just these two string properties: ThumbnailPath and FileName. When user selects a folder, a BackgroundWorker gets a list of files and creates instances of the File class. These instances are dispatched to the UI thread (in groups of 10) using BW's ReportProgress. In the event handler they are added to the ObservableCollection bound to my ListBox.
The problem is when at least 20-30 Files are to be added to my collection; the UI freezes for almost three seconds before the ListBox gets updated. Don't even ask what happens when a folder contains hundreds of files. Everything is propely prepared in the background, so I guess the problem arises when WPF starts to initialize and render empty Image elements. When I comment out the Image from the DataTemplate, it takes a blink of an eye to update the collection and its view.
Is there anything that can be done about this? I know could create the whole View object in the background thread (a new StackPanel, add children new Label and new Image, set values), but the whole point of DataBinding and templating should be to avoid the need to do this... So how do I fill a ListBox with an Image in its DataTemplate without losing responsiveness?
PS: The actual thumbnails are generated by FFmpeg and saved to a file, but this process only starts after all items (with blank image object) are displayed, so they are of no concern in the context of this question.
Try this:
Change ThumbnailPath to the type of BitmapImage;
when setting property use
BitmapImage bi1 = new BitmapImage();
// BitmapImage.UriSource must be in a BeginInit/EndInit block
bi1.BeginInit();
bi1.UriSource = new Uri(#"C:\filepath.jpg");
To save significant application memory, set the DecodePixelWidth or
// DecodePixelHeight of the BitmapImage value of the image source to the desired
// height or width of the rendered image. If you don't do this, the application will
// cache the image as though it were rendered as its normal size rather then just
// the size that is displayed.
// Note: In order to preserve aspect ratio, set DecodePixelWidth
// or DecodePixelHeight but not both.
bi1.DecodePixelWidth = 200;
bi1.EndInit();
bi1.Freeze();
//if you do not Freeze, your app will leak memory.
Related
I have a list box that has a list of user controls. Each user control has 5 combo boxes. I want to be able to read the selected text of each combo box in each user control from the main application. However, when I change the selection in the combo box, the text property of the combo box in the user control doesn't change when I read it in the main application.
Code-behind:
radQueryParamList.Items.Add(new TCardQueryParameters());
Xaml (This is just a data template for how to display a TCardQueryParameters object):
<DataTemplate x:Key="TCardViewQueryParamDataTemplate">
<tcardqueryparam:TCardQueryParameters x:Name="TCardViewerParamUC" />
</DataTemplate>
<telerik:RadListBox Grid.Column="1" ItemTemplate="{StaticResource TCardViewQueryParamDataTemplate}" Name="radQueryParamList" VerticalAlignment="Top" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Grid.ColumnSpan="3">
Where I loop over the list of user controls
string test = radTESTGACC.Text;//TEST combo box, Text property changes
//radQueryParamList is a listbox of user controls where TCardQueryParameters is the UC
foreach(TCardQueryParameters param in radQueryParamList.Items)
{
//Each UC has a radGACC combo box in it, and I am reading what the user
//selected for each user control here in the main app, but the text property
//never changes
String gacc = param.radGACC.Text; //Text property DOESN'T CHANGE
}
I thought that each instance of the user control would keep its own state and I would just be able to read what the user selected for that combo box, but that doesn't seem to be the case.
You have not bound the SelectedItem, SelectedValue, or SelectedIndex property of your internal ComboBox to anything so it maintains its selection.
An ItemTemplate is like a cookie cutter. It contains the definition of the object, but not the object itself. Properties specific to the object's state are lost unless they are bound to something on the DataContext behind the template.
This is important to note for two aspects.
First off, to improve performance WPF usually unloads items which are not visible, which often results in items being re-created from their template anytime they are reloaded. An example would be when you minimize an application to the taskbar, then maximize it again. This is usually better on performance and memory usage, however it does mean you have to be sure you store the state of items that were created with a Template somewhere.
And second, by default ListBoxes use something called virtualization. A simple way of explaining this would be this:
Suppose you have a ListBox of 100,000 items. In your ListBox, only 10 items can be visible at a time. WPF will render roughly 14 items (the 10 visible ones, and then a few extra for a scroll buffer so you don't see anything unusual while scrolling). When you scroll to new items, WPF just re-uses the existing items that are already rendered, and just replaces the DataContext behind those items.
As you can guess, it is far better on performance to render 14 UI items instead of 100k items.
So to answer your question, you will probably want to bind either SelectedItem, SelectedValue, or SelectedIndex of your TCardQueryParameters UserControl to a property on the DataContext (which in your case appears to be another different UserControl).
It should probably be noted that what you are essentially doing is creating a list of UserControls, assigning them to the ListBox, and then telling the ListBox that it should draw each UserControl with another separate UserControl. So although you are changing the SelectedItem in the template UserControl, that change is not being reflected to your ListBox.Items copy of the UserControl.
I'm guessing you probably don't want that, so would recommend removing your ItemTemplate completely.
Or better yet, create a new class object containing all the data for your UserControl, and adding that to your ListBox.Items. Then tell the ListBox to draw that item using a TCardQueryParameters UserControl as the ItemTemplate, like you have now.
I am trying to place a collection of images at certain places on a canvas through Binding.
For some reason, the images are displaying but are NOT at the locations which I have specified.
C#
_roomView.Room = new Room
{
Items = new List<Item> {
new Item {ImageUri = "/Escape;component/Images/Items/a.jpg", XPosition = 190, YPosition = 50},
new Item {ImageUri = "/Escape;component/Images/Items/b.png", XPosition = 390, YPosition = 100},
new Item {ImageUri = "/Escape;component/Images/Items/b.png", XPosition = 490, YPosition = 600}}
};
listBoxItems.ItemsSource = _roomView.Room.Items;
XML
<Canvas>
<Image Source="{Binding ImageUri}" Stretch="None" Canvas.Left="{Binding Room.Items.XPosition}" Canvas.Top="{Binding Room.Items.YPosition}"/>
</Canvas>
XPosition and YPosition are of type int. I have tried changing them to double but the images still aren't being displayed where I want them to be. They only get displayed at the top left of the screen - on top of each other.
Can anyone see the problem?
You're using the pack URI format which is only valid if your images have a Build Action of "Embedded Resource". Even then, I'm not sure this URI format is actually supported for Image.Source and if the URI has to be Relative or Absolute. Check the images Build Action (Right click --> Properties on those files) and try changing the UriKind.
Either way, unless you have a very good reason it's best to keep media assets as:
Build Action = Content and use the normal path URIs. Adding images to DLLs as Embedded Resources is bad for startup performance since it takes longer to load your bigger DLLs into the AppDomain.
It's also bad from a memory usage perspective since the DLLs loaded into memory are now bigger.
Bottom line:
<Image Source="myPicture.png" /> format is much better for performance than
<Image Source="myAssemblyName;component/myPicture.png" /> syntax.
Your C# code has collection of images.
Your XAML only shows a single image. If the XAML is in the listbox data template, you'll get several canvases, with 1 image each.
To display a collection of images on the single Canvas, you could use e.g. <ItemsControl>, set ItemsPanel to a Canvas, and in the data template you specify a single image with source, canvas.left and canvas.top.
If you're trying to do what I've described with a ListBox instead of ItemsControl, it wont work because it has one more layer of UI elements between ItemsPanel and item, namely the item container.
P.S. Fastest way to troubleshoot such GUI problems - XAML Spy, I've bought myself a personal license. Evaluation is 21 days long, fully functional.
I am defining a strategy where a main view will use data templates to switch between the views. Currently it can switch between 3 Views:
ApplicationView: it is actually the view that consists of lots of
different views, mostly layered out using tabs / docking. this is a
view that deals with application data.
LogInView: it is used for logging the user in.
DialogView: it is used to display dialog views. This view will also use data templates to select a proper view that is required.
The idea is that when a dialog view needs to be displayed, it is set as current view on the main view. After the selection is done, this information is passed to ApplicationView, or a view that is part of ApplicationView. While DialogView is shown, ApplicationView, must not be released from memory, since it ApplicationViewModel will still be manipulating with data (it needs to constantly work in the background).
I am thinking of achieving this using DataTemplates, and binding ContentControl's Content to CurrentView:
// in MainView
DataTemplate DataType="{x:Type vm:ApplicationViewModel}">
<vw:ApplicationView />
</DataTemplate>
.....
// in MainViewModel
public ViewModelBase CurrentView { get; set; }
Basically I am trying to avoid using modal windows for dialogs.
1) Is this strategy OK, or there are some problems that I am not aware with it?
2) When I switch to DialogView (I am actually switching viewmodels), what happens with the ApplicationView/ApplicationViewModel? Do I need to store ApplicationViewModel's reference somewhere, so it doesn't get garbage collected? I haven't tested this, but probably when I set CurrentView a new instance of ViewModel/View will be created.
3) Connected to second question, when using DataTemplates, what happens to View/ ViewModel that was previously used, and is now replaced with different view/viewmodel?
I don't see anything wrong with the way you're switching views, although typically you don't want to get rid of the application when you're displaying a dialog.
What I've done in the past is to put both the CurrentView, and the DialogView in a Grid so they are positioned on top of each other, then have the ApplicationViewModel contain an IDialogViewModel and IsDialogVisible properties, and when you want to display the dialog simply populate those two fields. (see below for an example)
You will have to store the ApplicationViewModel somewhere if you want to go back to it and avoid creating a new ApplicationViewModel
WPF disposes of UI objects that are no longer visible, so switching the CurrentView from Login to Application will get rid of the LoginView and create an ApplicationView
The ContentControl's Content is getting set to your ViewModel, so the ViewModel is actually being put in the applications VisualTree. Whenever WPF encounters an object in its VisualTree that it doesn't know how to draw, it will draw it using a TextBlock containing the .ToString() of the the object. By defining a DataTemplate, you are telling WPF how to draw the object instead of using its default .ToString() method. Once the object leaves the VisualTree, any visual objects that were created to render the object will get destroyed.
Although I would keep using what you currently have for switching Views, I would not use that method for the Login, Application, and Dialog views.
Typically the LoginView should only be displayed once when logging in, although it might get displayed again in a Dialog if you allow users to switch logins once logging in. Because of this, I typically show the LoginView in the startup code, then display the ApplicationView once login is successful.
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var login = new LoginDialog();
var loginVm = new LoginViewModel();
login.DataContext = loginVm;
login.ShowDialog();
if (!login.DialogResult.GetValueOrDefault())
{
Environment.Exit(0);
}
// Providing we have a successful login, startup application
var app = new ApplicationView();
var context = new ApplicationViewModel(loginVm.CurrentUser);
app.DataContext = context;
app.Show();
}
Like I said earlier, I wouldn't want to hide the Application when I display a Dialog, so I would make the Dialog part of the Application
Here's an example of the DataTemplate I would use for my ApplicationViewModel, using my own custom Popup from my blog for the Dialog
<Grid x:Name="ApplicationView">
<ContentControl Content="{Binding CurrentView}" />
<local:PopupPanel x:Name="DialogPopup"
Content="{Binding DialogContent}"
local:PopupPanel.IsPopupVisible="{Binding IsDialogVisible}"
local:PopupPanel.PopupParent="{Binding ElementName=ApplicationView}" />
</Grid>
Personally, I would find it easier to use ZOrdering within a standard grid, and put everything in the same view - using the ViewModel to manage visibility.
E.g
<Grid>
<Grid Visibility="{Binding IsView1Visible,
Converter={StaticResource BoolToVisibilityConverter}}">
<!-- view 1 contents -->
</Grid>
<Grid Visibility="{Binding IsView2Visible,
Converter={StaticResource BoolToVisibilityConverter}}">
<!-- view 2 contents -->
</Grid>
<Grid Visibility="{Binding IsView3Visible,
Converter={StaticResource BoolToVisibilityConverter}}">
<!-- view 3 contents -->
</Grid>
<Grid Visibility="{Binding IsDialogVisible,
Converter={StaticResource BoolToVisibilityConverter}}">
<!-- dialog contents contents -->
</Grid>
</Grid>
I'm developing an app for Windows Phone 7 that populates a WrapPanel with a list of objects retrieved from an ObservableCollection<Photo>.
<ListBox ItemsSource="{Binding Photo}">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding File}" />
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<toolkit:WrapPanel ItemHeight="150" ItemWidth="150" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
It's working how it's supposed to work (in the means that it's loading all the images), but I'm having some performance issues since WrapPanel : Panel. So it doesn't virtualize any data, loading all the Image objects of the ListBox, even the ones that the user can't see.
This approach is OK when ObservableCollection<Photo>.Count <= 30 but as the Collection gets bigger and bigger things start to get slow.
Since the user can have up to 1000 images, it's simply not going to work this way. Even though I'm binding Thumbnails to display the Image object.
I've tried to use David Anson's LowProfileImageLoader to create Images off the UI thread. And to bind the Images as the user scrolls the ListBox. But it doesn't work since it's expecting an UriSource and I'm actually binding a BitmapImage to the Image.Source, because the images are beeing loaded from IsolatedStorage.
public class Photo : INotifyPropertyChanged, INotifyPropertyChanging
{
...
...
public BitmapImage File
{
get
{
// Thumbnail
string filePath = Path.Combine("Images", FileName);
byte[] data;
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream isfs = isf.OpenFile(filePath, FileMode.Open, FileAccess.Read))
{
data = new byte[isfs.Length];
isfs.Read(data, 0, data.Length);
isfs.Close();
}
}
MemoryStream ms = new MemoryStream(data);
BitmapImage bi = new BitmapImage();
bi.SetSource(ms);
return bi;
}
}
}
Can anyone help me in this task of making the content (photos) load as the user sees it? Is there anything such as a VirtualizingWrapPanel or WrapPanel : VirtualizingPanel?
Thanks. If any code snippet is needed feel free to ask.
Loading the images to memory from the storage when the user scrolls will make your UI jittery especially when the user tries to scroll or see more elements very quickly.
But, if you have all the images in memory and then would like to bind the images to the listbox on scrolling, you could use the VirtualizingStackPanel. A wrap panel would need to know the size of the images to display correctly. A workaround is possible if you know the size of the images and if they're all the same.
You could customize the VirtualizingStackPanel and position the images in rows like the way they would in a wrappanel, or any other way you like.
Hope that helps.
Although,
I am not sure if this will solve the problem when you have 1000 heavy images. But it is worth a try, if you haven't done so.
EDIT: This article might help.
Try creating a wrap panel from a virtualized listbox.
Here is a link how to do it. it works pretty good, but it has 1 problem... it will have a tilt effect on a whole line instead of an item... I'm currently investigating that problem.
I have a datagrid with a template column. The template has an image:
<Image HorizontalAlignment="Left" Name="ImageProduct" Stretch="None" VerticalAlignment="Center" Grid.Row="0" Grid.Column="0" Source="{Binding Path=ProductImage, Mode=OneWay}" RenderOptions.BitmapScalingMode="HighQuality"/>
The grid scrolls very slowly because the ProductImage lazilly loads a private bitmapimage object as the user scrolls the grid. I was thinking of using another thread to load the private variable (behind the ProductImage property). I'm getting into trouble with my code because of various reasons...one exception was I can only update UI on the STA thread and another was a dependency source can't be in a different thread than the dependency sink (?)
I can't think of a good way to do this. The code for the grid looks something like this with a failed attempt at the backgroundworker:
var productVMList = GetProducts();
_window.ReceivingBatchProductsGrid.ItemsSource = productVMList;
var setProductImageWorker = new BackgroundWorker();
setProductImageWorker.DoWork += setProductImageWorker_DoWork;
setProductImageWorker.RunWorkerAsync(productVMList);
And here is DoWork:
var products = (ObservableCollection)e.Argument;
foreach (var product in products)
{
product.SetProductImage();
}
Any thoughts?
Normally, dependency objects can only be used on the thread that created them. However, the ones that inherit from Freezable (like ImageSource for instance) can be used from another thread, as long as they are frozen. So when you create your ImageSource objects on another thread, you just need to call Freeze on them before you send them to the UI, and it should work fine.
An easy way to make the images load asynchronously is to use the Binding.IsAsync property:
<Image ... Source="{Binding Path=ProductImage, Mode=OneWay, IsAsync=True}" ... />
That way you don't need to worry about creating a new thread and updating the target property when the image is loaded, it's handled automatically by WPF.