WPF - Loading many images into ObservableCollection One by One - wpf

In WPF, I'm trying to load many (thousands) of images into a ListBox WrapPanel.
I'm attempting to load the images similar to how a Windows Explorer window does.
So far I have my code which loads all the images' name, location (as tag), and a placeholder image to speed up load time:
Dim ofd As New Microsoft.Win32.OpenFileDialog()
ofd.Multiselect = True
ofd.Filter = "JPEG|*.jpg"
If ofd.ShowDialog() Then
For Each f In ofd.FileNames
Items.Add(New With {.img = New BitmapImage(New Uri("pack://application:,,,/Resources/PlaceholderPhoto.png")), .nam = Path.GetFileNameWithoutExtension(f), .tag = f})
Next
'The name of my ObservableCollection is Items'
lstboxPhotos.ItemsSource = Items
End If
That part is fine. What I'm trying to do after is load the images' thumbnails dynamically (one-by-one). I'm doing this so the user can interact with and see how many images are available while the thumbnails are loading. I've tried a couple different things - a background worker, dispatcher, and separate thread.
The code I'm using to load the images is below:
'i came from me doing a for..next for each image in Items collection'
Dim bmp As New BitmapImage()
bmp.BeginInit()
bmp.DecodePixelWidth = 90
bmp.DecodePixelHeight = 60
bmp.CacheOption = BitmapCacheOption.OnLoad
bmp.UriSource = New Uri(Items.Item(i).tag, UriKind.Absolute)
bmp.EndInit()
Items.Item(i).img = bmp
I've search all over the internet. I'm honestly not sure what direction to take in order to do what I'm needing to.
Let me know if I need to clarify anything else. Thank you in advance! :)
Edit:
Okay so referring to this article, using the 2nd answer I got the items to load one at a time. It loads all of the items' names just fine, but seems to stop loading the images after about 40 items.
If anyone could shed light on why it might cease loading the thumbnails, but continue loading the items' names that would be a great help! Thanks!

You can do that quickly and easily using the built in TypeConverter that converts string file paths into ImageSource objects. Take this simple example that will show thumbnails of all of the images in your Pictures folder:
public ObservableCollection<string> FilePaths { get; set; }
...
FilePaths = new ObservableCollection<string>(Directory.GetFiles(
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)));
...
<ItemsControl ItemsSource="{Binding FilePaths}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}" Width="100" Stretch="Uniform" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
In this example, each Image.Source property is data bound directly with one of the file paths in the FilePaths collection.

Okay I know it's been a while, but I wanted to post what I ended up doing.
First, I loaded all the image names into the ListBox normally using an ObservableCollection with a temporary image:
Dim Items As New ObservableCollection(Of Object)
Dim Files() As String
...
For Each f In Files
Items.Add(New With {.img = New BitmapImage(New Uri("/Resources/Photo.png")), .name = f})
Next
lbPhotos.ItemsSource = Items
Then I used a BackgroundWorker to replace each placeholder image with the actual image:
Private WithEvents bw As New BackgroundWorker
Private Sub bw_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles bw.DoWork
...
For i = 0 To Items.Count - 1
If bw.CancellationPending Then
e.Cancel = True
Else
Dim n As String = Items(i).name
Dim t As String = Items(i).tag
Dim bmp As New BitmapImage
bmp.BeginInit()
bmp.UriSource = New Uri(PathToImage, UriKind.Absolute)
bmp.EndInit()
bmp.Freeze()
Dispatcher.BeginInvoke(Sub()
Items.RemoveAt(i)
Items.Insert(i, New With {.img = bmp, .name = n})
End Sub)
End If
Next
End Sub
That allows for the user to interact with the UI while the images load.

Related

Set icon for MenuItem VB

i need to create the context menu from code behind in WPF.
Everything works great except the Icon, i set the MenuItem icon like this
Dim tsmi As New MenuItem() With {
.Header = cmd.Name,
.Icon = cmd.Icon,
.Tag = cmd
}
where cmd.Icon is a System.Drawing.Image.
What i get instead of the Icon is the string System.Drawing.Image where it should be the Image.
Can anyone help?
System.Drawing.Image is from WinForms, what you need is a System.Windows.Controls.Image.
You can make one like this:
New Image() With {.Source = New BitmapImage(New Uri("pack://application:,,,/Your.Assembly.Name;component/Images/image.png"))}
...where you have a file called image.png (marked with Build Action=Resource) in a folder Images in the assembly Your.Assembly.Name.dll.
The MenuItem documentation shows this XAML:
<MenuItem Header="New">
<MenuItem.Icon>
<Image Source="data/cat.png"/>
</MenuItem.Icon>
</MenuItem>
So you can clearly use a WPF Image control for the icon. The documentation for the Image.Source property provides a link to a topic entitled "How to: Use the Image Element" and it includes this code example:
' Create Image Element
Dim myImage As New Image()
myImage.Width = 200
' Create source
Dim myBitmapImage As New BitmapImage()
' BitmapImage.UriSource must be in a BeginInit/EndInit block
myBitmapImage.BeginInit()
myBitmapImage.UriSource = New Uri("C:\Documents and Settings\All Users\Documents\My Pictures\Sample Pictures\Water Lilies.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.
myBitmapImage.DecodePixelWidth = 200
myBitmapImage.EndInit()
'set image source
myImage.Source = myBitmapImage
That pretty much gives you everything you need. I have never used any of these types or members before. I just spent some time reading the relevant documentation.

WPF VB.NET ComboBox DataBinding

I'm new to LINQ to SQL and WPF and i'm using several online guides and YouTube Videos to pick this up (along with this application).
The problem:
On loading the window (and on other key data changes that refresh the Windows DataContext), I need to update what selected item the ComboBox is showing to the item with a matching ID from the Window DataContext binding (c_Instance.FK_RiskIndicator_ID)
I populate the ComboBox from a view which returns the display field and the ID (All ID's are GUIDs).
When the selection is changed, i call a Stored Procedure to update the DB with the ID of the selected item.
Private db As New AppDatabase_DBDataContext
Private CRCTypeView As BindingListCollectionView
Private InstanceDetailTypeView As BindingListCollectionView
Property guInstance_ID As Guid
Private Sub Refresh_PCA_Instance()
Dim c_Instance As IEnumerable(Of t_Instance)
c_Instance = (From Inst In db.t_Instances Where Inst.ID = guInstance_ID Select Inst)
If c_Instance.Count = 1 Then
Me.DataContext = c_Instance
Me.InstanceDetailTypeView = CType(CollectionViewSource.GetDefaultView(Me.DataContext), BindingListCollectionView)
ElseIf c_Instance.Count > 1 Then
Me.DataContext = c_Instance.First
Me.InstanceDetailTypeView = CType(CollectionViewSource.GetDefaultView(Me.DataContext), BindingListCollectionView)
End If
End Sub
Private Sub LoadCRCType()
Dim CRCTypeList As IEnumerable(Of vw_RiskIndicator)
CRCTypeList = (From RiskIndicator In db.vw_RiskIndicators
Order By RiskIndicator.IndexNum
Select RiskIndicator)
PCA_CRC_ComboBox.DataContext = CRCTypeList
Me.CRCTypeView = CType(CollectionViewSource.GetDefaultView(PCA_CRC_ComboBox.DataContext), BindingListCollectionView)
End Sub
Private Sub PCA_CRC_ComboBox_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Handles PCA_CRC_ComboBox.SelectionChanged
If PCA_CRC_ComboBox.SelectedItem IsNot Nothing Then
db.sp_Instance_RiskIndicator_UPSERT(guInstance_ID, PCA_CRC_ComboBox.SelectedValue)
Refresh_PCA_Instance() 'Refreshes the Window DataContext
End If
End Sub
XAML for the ComboBox:
<ComboBox x:Name="PCA_CRC_ComboBox" Grid.Row="1" Grid.Column="2"
ItemsSource="{Binding}"
SelectedValuePath="ID"
DisplayMemberPath="RiskIndicator"
PreviewKeyDown="RiskIndicator_ComboBox_PreviewKeyDown"/>
I had assumed i could do this with binding the SelectedItem, but i can't figure it out. I had assumed it would be something like:
<ComboBox x:Name="PCA_CRC_ComboBox" Grid.Row="1" Grid.Column="2"
SelectedItem="{Binding Path=FK_RiskIndicator_ID}" <!--THIS LINE HERE-->
ItemsSource="{Binding}"
SelectedValuePath="ID"
DisplayMemberPath="RiskIndicator"
PreviewKeyDown="RiskIndicator_ComboBox_PreviewKeyDown"/>
Any help with this would be much appreciated.
EDIT: Grouped the code together. I assume this is what was meant. Personally i think this layout if less explanatory.

How can I force a TabItem to initialize content on load?

[disclaimer: I am new to Visual Basic.]
In a WPF, I have a TabControl containing 2 TabItems:
The first TabItem contains a bunch of URLs.
The second TabItem consists of a DockPanel that contains a cefSharp webView (chromium embedded for .net)
When I click on a url in tab1 it loads a page in the browser contained in tab2... But, it only works if I have initialized the browser first by clicking on tab2.
After doing some searching, it looks like vb.net doesn't initialize the content in a TabItem until it becomes visible. (right?)
So, my question is, how can I force a non-selected tab to initialize its content on load, in the background? (ie. so I don't have to click on the tab or switch to it first)
EDIT:
As requested, here is the relevant code:
The relevant XAML consists of a single DockPanel named "mainBox"
<DockPanel Name="mainBox" Width="Auto" Height="Auto" Background="#afe0ff" />
And here is my "code behind" vb script:
Class MainWindow : Implements ILifeSpanHandler, IRequestHandler
Shared web_view1 As CefSharp.Wpf.WebView
Shared web_view2 As CefSharp.Wpf.WebView
Public Sub init(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Loaded
'This is in a DockPanel created on the xaml named mainBox
' set up tabControl:
Dim browserTabs As New TabControl()
browserTabs.BorderThickness = Nothing
Dim tab1 As New TabItem()
tab1.Header = "My Tab"
Dim tab2 As New TabItem()
tab2.Header = "Browser"
Dim tab1Content As New DockPanel()
Dim tab2Content As New DockPanel()
tab1.Content = tab1Content
tab2.Content = tab2Content
browserTabs.Items.Add(tab1)
browserTabs.Items.Add(tab2)
mainBox.Children.Add(browserTabs)
' set up browsers:
Dim settings As New CefSharp.Settings()
settings.PackLoadingDisabled = True
If CEF.Initialize(settings) Then
web_view1 = New CefSharp.Wpf.WebView()
web_view1.Name = "myTabPage"
web_view1.Address = "http://stackoverflow.com/"
web_view2 = New CefSharp.Wpf.WebView()
web_view2.Name = "browserPage"
web_view2.Address = "https://www.google.com"
web_view2.LifeSpanHandler = Me
web_view2.RequestHandler = Me
AddHandler web_view2.PropertyChanged, AddressOf web2PropChanged
tab1Content.Children.Add(web_view1)
tab2Content.Children.Add(web_view2)
End If
End Sub
End Class
So, in its default state, tab1 is showing at start up -- the browser on tab2 (web_view2) won't initialize until I click its tab or change to its tab via script. Hope this clears it up a bit.
Your code doesn't use Windows Forms' TabPage but perhaps this might still help
As we know, Controls contained in a TabPage are not created until the
tab page is shown, and any data bindings in these controls are not
activated until the tab page is shown. This is by design and you can
call TabPage.Show() method one by one as a workaround.
via MSDN forums
Also, based on the above idea, have you tried the following.
tab2.isEnabled = True
tab1.isEnabled = True
Or:
tab2.Visibility = True
tab1.Visibility = True
Also, the BeginInit Method might help in your situation.
I had the same problem but found it initializes if I use the name of the tabpage and show (tabName1.show()) on the form load code for each page ending with the one I want displayed:
tabPage2.show()
tabPage3.show()
tabPage4.show()
tabPage5.show()
tabPage1.show()
It ripples thru and you never see the pages changing but it works. The other suggestions didn't work for me in Visual Studio 2010.

How To Link ComboBoxItem to File

This question is a follow-up to one I asked and got answered here: How to display XPS document using a selected combobox item
I've created a WPF app using VB 2010. I set the comboboxitems via the XAML. However, I can't seem to figure out how to set the value of each item to a file path.
The objective is for a user to be able to select an item from a drop-down list, then that selection opens an XPS file in the DocumentViewer. The code below was provided to me by COMPETENT_TECH (thanks) to read and display the value of the selected comboboxitem in the DocumentViewer.
The path to the files I want opened is C:\folder\file.xps
Private Sub Button4_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles Button4.Click
Try
Dim sFileName As String
sFileName = DirectCast(ComboBox1.SelectedValue, String)
If Not String.IsNullOrEmpty(sFileName) Then
Dim theDocument As New System.Windows.Xps.Packaging.XpsDocument(sFileName, System.IO.FileAccess.Read)
DocumentViewer1.Document = theDocument.GetFixedDocumentSequence()
End If
Catch ex As Exception
MessageBox.Show("ERROR: " & ex.Message)
End Try
End Sub
Thanks in advance for your assistance.
Update
Here's the XAML I'm using:
<ComboBox Width="Auto" IsReadOnly="True" IsEditable="True" Name="ComboBox1" Height="Auto" Margin="0" Padding="1" Grid.Column="2">
<ComboBoxItem>123456</ComboBoxItem>
<ComboBoxItem>123457</ComboBoxItem>
<ComboBoxItem>123458</ComboBoxItem>
</ComboBox>
Depending on precisely how the xaml to load the combobox is specified, you will probably want to change this line:
Dim theDocument As New System.Windows.Xps.Packaging.XpsDocument(sFileName, System.IO.FileAccess.Read)
to:
Dim theDocument As New System.Windows.Xps.Packaging.XpsDocument(System.IO.Path.Combine("C:\folder", sFileName & ".xps"), System.IO.FileAccess.Read)
All that we do in the new code is combine the directory where the files are stored with the file name retrieved from the combobox.
Update
The correct way to retrieve the value from the combobox is:
If ComboBox1.SelectedValue IsNot Nothing Then
sFileName = DirectCast(ComboBox1.SelectedValue, ComboBoxItem).Content.ToString()
End If
I highly recommend you use Data Binding with that ComboBox.
create a class something like following:
Class XPSDocumentInfo
{
Public Property Title As String
Public Property FileName As String
}
Create an ObservableCollection(Of XPSDocumentInfo) and bind it to ComboBox's ItemsSource.
Use DisplayMemberPath="Title" attribute on ComboBox so that it will use Title property to show text in dropdown but since you bound collection of type XPSDocumentInfo, SelectedItem property of that ComboBox with return an object of type XPSDocumentInfo.
For example,
sFileName = DirectCast(ComboBox1.SelectedValue, String)
will be changed to
sFileName = DirectCast(ComboBox1.SelectedItem, XPSDocumentInfo).FileName

WPF DataGrid Row add in codebehind

I am from VB.Net WinForms comming. Now I wanted to write a small app in WPF, listing some files in a datagridview. I used WPF's DataGrid, created some Columns. And then failed to add my rows.
Please, can you help me to select the right way to get my filenames, state-text and thumbnails added to the DataGrid Row?
In VB.Net WinForms I can add a row like this:
Datagridview1.Rows.add(Myvalue, "RowStateText", "Hello World", MyDate)
In WPF's DataGrid I can add
DataGrid1.Items.Add(New DataGridRow())
But how to fill my DataGridRow?
Private Sub AddFilesAndFolders(ByVal Base As IO.DirectoryInfo, ByRef dgv As DataGrid)
'For Each di As IO.DirectoryInfo In Base.GetDirectories
' Call AddFilesAndFolders(di, dgv)
'Next
Dim item As DataGridRow
For Each fi As IO.FileInfo In Base.GetFiles
item = New DataGridRow'<-- test 1 (row is added but empty)
Dim di As New MyFileInfo'<-- test 2 (my own class with public members, but how to add as row with declared columns?)
di.FileName = fi.FullName
di.FileDate = fi.LastAccessTime
item.Item = fi.FullName
dgv.Items.Add(di)
Next
End Sub
Hi: you should set an ItemsSource instead of adding items manually. If the columns are set up correctly then it will just 'work'!
dbv.ItemsSource = Base.GetFiles
or
dbv.ItemsSource = CreateMyFileInfos(Base.GetFiles)
If you have any more problems, please post back here.
Edit: on second inspection it looks like you may want to be doing it recursively. In which case your AddFilesAndFolders could instead be CreateFilesAndFolders, which would return a collection of FileInfo/MyFileInfo objects, merged with the collections produced by the child folders recursively; then bind the whole list returned from the first call, to the grid.
Hope that helps!
WPF is a mindset change, you need to get away from the Winforms way of thinking.
Ultimately you need to set the ItemsSource to an IEnumerable, preferably a ObservableCollection.
The quickest way to get started would be to put the ObservableCollection as a public property in your code-behind file:
public ObservableCollection<DirectoryInfo> files { get;set; }
Then in the constructor or a Load event on the Window, populate the collection with your data and then add to the Xaml declaration for your DataGrid:
ItemsSource = "{Binding Path=files}"
EDIT:
I tried this out using the DirectoryInfo class, in my code behind I added:
public ObservableCollection<DirectoryInfo> Dir = new ObservableCollection<DirectoryInfo>();
public Window1()
{
InitializeComponent();
Dir.Add(new DirectoryInfo("c:\\"));
Dir.Add(new DirectoryInfo("c:\\temp\\"));
dataGrid1.ItemsSource = Dir;
}
For some reason this was not working using the Databinding via Xaml, but I did not try very hard to get it to work.

Resources