Setting User Control's DataContext from Code-Behind - wpf

This should be pretty easy, but it throws VS2008 for a serious loop.
I'm trying out WPF with MVVM, and am a total newbie at it although I've been developing for about 15 years, and have a comp. sci. degree. At the current client, I am required to use VB.Net.
I have renamed my own variables and removed some distractions in the code below, so please forgive me if it's not 100% syntactically perfect! You probably don't really need the code to understand the question, but I'm including it in case it helps.
I have a very simple MainView.xaml file:
<Window x:Class="MyApp.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Main Window" Height="400" Width="800" Name="MainWindow">
<Button Name="Button1">Show Grid</Button>
<StackPanel Name="teststack" Visibility="Hidden"/>
</Window>
I also have a UserControl called DataView that consists of a DataGrid:
<UserControl x:Class="MyApp.Views.DataView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WpfToolkit="http://schemas.microsoft.com/wpf/2008/toolkit" >
<Grid>
<WpfToolkit:DataGrid
ItemsSource="{Binding Path=Entries}" SelectionMode="Extended">
</WpfToolkit:DataGrid>
</Grid>
</UserControl>
The constructor for the DataView usercontrol sets up the DataContext by binding it to a view model, as shown here:
Partial Public Class DataView
Dim dataViewModel As ViewModels.DataViewModel
Public Sub New()
InitializeComponent()
dataViewModel = New ViewModels.DataViewModel
dataViewModel.LoadDataEntries()
DataContext = dataViewModel
End Sub
End Class
The view model for DataView looks like this (there isn't much in ViewModelBase):
Public Class DataViewModel
Inherits ViewModelBase
Public Sub New()
End Sub
Private _entries As ObservableCollection(Of DataEntryViewModel) = New ObservableCollection(Of DataEntryViewModel)
Public ReadOnly Property Entries() As ObservableCollection(Of DataEntryViewModel)
Get
Return _entries
End Get
End Property
Public Sub LoadDataEntries()
Dim dataEntryList As List(Of DataEntry) = DataEntry.LoadDataEntries()
For Each dataentry As Models.DataEntry In dataEntryList
_entries.Add(New DataEntryViewModel(dataentry))
Next
End Sub
End Class
Now, this UserControl works just fine if I instantiate it in XAML. When I run the code, the grid shows up and populates it just fine.
However, the grid takes a long time to load its data, and I want to create this user control programmatically after the button click rather than declaratively instantiating the grid in XAML. I want to instantiate the user control, and insert it as a child of the StackPanel control:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
Dim dataView As New DataView
teststack.Children.Add(dataView)
End Sub
When I do this, as soon as the Button1_Click finishes, my application locks up, starts eating RAM, and hits the CPU about 50%.
Am I not instantiating my UserControl properly? It all seems to come down to the DataContext assignment in DataEntry's constructor. If I comment that out, the app works as expected (without anything in the grid, of course).
If I move this code block into Button1_Click (basically moving DataEntry's constructor code up a level), the app still fails:
dataViewModel = New ViewModels.DataViewModel
dataViewModel.LoadDataEntries()
dataView.DataContext = dataViewModel
I'm stumped. Can anybody give me some tips on what I could be doing wrong, or even how to debug what infinite loop my app is getting itself into?
Many thanks.

The root cause of your issue appears to be either the raw amount of data you're loading or some inefficiency in how you load that data. Having said that, the reason you're seeing the application lock up is that you're locking the UI thread when loading the data.
I believe that in your first case the data loading has been off loaded onto another thread to load the data. In you second example you're instantiating the control on the UI thread and as a result all the constructor and loading logic is performed on the current thread (the UI thread). If you offload this work onto another thread then you should see similar results to the first example.

I eventually gave up on trying to get the DataContext on the UserControl set during instantiation of the UserControl (either in XAML or code). Now I load up the data and set the DataContext of the UserControl in an event in the UserControl (IsVisibleChanged, I believe). When I instantiate the UserControl in XAML, I have it's Visibility set to Hidden. When Button1 is clicked, I set the UserControl's Visibility to Visible. So the UserControl pops into view, and it loads up its data and DataContext is set. Seems to work, but also seems very kludgey. :-( Thanks for the help, folks!

If it's only a matter of your control taking a long time to populate data, you should populate the control on another thread then add it through a delegate:
Since I'm not too good at writing VB.NET, but here's the C# equivalent:
private void Button1_Click(Object sender, RoutedEventArgs e)
{
Thread thr = new Thread(delegate()
{
DataView dataView = new DataView();
this.Dispatcher.BeginInvoke((Action) delegate()
{
teststack.Children.Add(dataView);
});
});
thr.Start();
}

Related

WPF Can I use a DataTrigger to have the View do something after the View Model changes the value of a Property?

My WPF MVVM VB.NET app loads a list of songs into a ListBox at start. The list contents populate in a BackgroundWorker that is kicked off in the Constructor of the ViewModel. Once this is done, I want to set focus to the first song in the list.
As setting this focus is purely a View operation, I want it in the code-behind of the XAML. It's no business of the ViewModel where focus goes.
I tried doing this on various Window and ListBox events, but they either don't fire, or fire too early. So I'm think what I need is a Boolean Property that the ViewModel sets when it's done loading the songs into the list. That's when I need the View to catch that Property Change, and call the code-behind function that has the logic to maniuplate the View, in the is case, setting focus on the first song in the list.
But this is where my knowledge of WPF is short. I searched and it sounds like DataTrigger could do the trick. But where to put it, and what's the right syntax, and how to have it call my code-behind function?
Or is there an even simpler way that I'm overlooking. This seems like a basic functionality - to trigger some code-behind action in the View when a Property changes a certain way in the ViewModel.
Here's the code-behind function. I can elaborate it once it's successfully getting called at the intended time:
Private Sub FocusSongsList()
' set focus back to the Songs list, selected item (couldn't just set focus to the list, it ran forever and looks like it set focus to every item in turn before releasing the UI)
Dim listBoxItem = CType(LstSongs.ItemContainerGenerator.ContainerFromItem(LstSongs.SelectedItem), ListBoxItem)
If Not listBoxItem Is Nothing Then
listBoxItem.Focus()
End If
End Sub
Here's my ListBox:
<ListBox x:Name="LstSongs" ItemsSource="{Binding FilteredSongs}" DisplayMemberPath="Path"
HorizontalAlignment="Stretch"
SelectionMode="Extended" SelectionChanged="LstSongs_SelectionChanged" Loaded="FocusSongsList"/>
And I would define a new property that can be set from the RunWorkerCompleted part of the BackgroundWorker.
Private _InitialSongLoadCompleted As Boolean
Public Property InitialSongLoadCompleted() As Boolean
Get
Return _InitialSongLoadCompleted
End Get
Set(ByVal value As Boolean)
_InitialSongLoadCompleted = value
RaisePropertyChanged("InitialSongLoadCompleted")
End Set
End Property
A DataTrigger can't execute methods. It can only set properties.
Focus can't be activated by setting a property, therefore a DataTrigger can't solve your problem.
Generally, if you have longrunning initialization routines you should move them to an init routine (which could be async) or use Lazy<T>.
For example, you instantiate your view model class and call Initialize() afterwards. After the method has returned you can continue to initialize the ListBox:
MainWindow.xaml.cs
Partial Class MainWindow
Inherits Window
Private ReadOnly Property MainViewModel As MainViewModel
Public Sub New(ByVal dataContext As TestViewModel, ByVal navigator As INavigator)
InitializeComponent()
Me.MinViewModel = New MainViewMdel()
Me.DataContext = Me.MainViewModel
AddHandler Me.Loaded, AddressOf OnLoaded
End Sub
' Use the Loaded event to call async methods outside the constructor
Private Async Sub OnLoaded(ByVal sender As Object, ByVal e As EventArgs)
Await mainViewModel.InitializeAsync()
' For example handle initial focus
InitializeListBox()
End Sub
End Class
MainViewModel.cs
Class MainViewModel
Inherits INotifyPropertyChanged
Public Async Function InitializeAsync() As Task
Await Task.Run(AddressOf InitializeSongList)
End Function
Private Sub InitializeSongList()
' TODO::Initialize song list
End Sub
End Class
It has been a long long time since I wrote much VB, so I'm afraid this is c# code.
You can handle targetupdated on a binding.
This fires when data transfers from the source ( viewmodel property ) to the target (the ui property and here itemssource)
<ListBox
x:Name="LstSongs"
ItemsSource="{Binding Songs, NotifyOnTargetUpdated=True}"
TargetUpdated="ListBox_TargetUpdated"/>
When you replace your list, that targetupdated will fire.
If you raise property changed then the data will transfer ( obviously ).
private async void ListBox_TargetUpdated(object sender, DataTransferEventArgs e)
{
await Task.Delay(200);
var firstItem = (ListBoxItem)LstSongs.ItemContainerGenerator
.ContainerFromItem(LstSongs.Items[0]);
firstItem.Focus();
Keyboard.Focus(firstItem);
}
As that data transfers, there will initially be no items at all of course so we need a bit of a delay. Hence that Task.Delay which will wait 200ms and should let the UI render. You could make that a bit longer or dispatcher.invokeasync.
It finds the first container and sets focus plus keyboard focus.
It might not be at all obvious that item has focus.
A more elegant approach using dispatcher will effectively schedule this focussing until after the ui has rendered. It might, however, look rather tricky to someone unfamiliar with c#
private void ListBox_TargetUpdated(object sender, DataTransferEventArgs e)
{
Dispatcher.CurrentDispatcher.InvokeAsync(new Action(() =>
{
var firstItem = (ListBoxItem)LstSongs.ItemContainerGenerator
.ContainerFromItem(LstSongs.Items[0]);
firstItem.Focus();
Keyboard.Focus(firstItem);
}), DispatcherPriority.ContextIdle);
}
If you want a blue background then you could select the item.
firstItem.IsSelected = true;
Or you could use some datatrigger and styling working with IsFocused.
https://learn.microsoft.com/en-us/dotnet/api/system.windows.uielement.isfocused?view=windowsdesktop-7.0
( Always distracts me that, one s rather than two and IsFocussed. That'll be US english I gues )
Here's my mainwindowviewmodel
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
private List<string> songs = new List<string>();
MainWindowViewModel()
{
Task.Run(() => { SetupSongs(); });
}
private async Task SetupSongs()
{
await Task.Delay(1000);
Songs = new List<string> { "AAA", "BBB", "CCC" };
}
}
I'm using the comunity toolkit mvvm for code generation. Maybe it does vb as well as c#.
You might accomplish your goal by defining a custom event in the viewmodel which is raised when the list processing is complete. The view can subscribe to it and act accordingly.
It would look something like this:
Class MyViewModel
'Custom eventargs shown for completeness, you can use EventHandler if you
'don't need any custom eventargs.
Public Event ListCompleted As EventHandler(Of ListCompletedEventArgs)
'...
Public Sub ProcessSongList()
'Note that if this runs on a background thread, you may need to
'get back on the UI thread to raise an event for the view to handle
RaiseEvent ListCompleted(Me, New ListCompletedEventArgs())
End Sub
End Class
Class MyView
Public Sub New(ByVal vm as MyViewModel)
Me.DataContext = vm
AddHandler vm.ListCompleted, AddressOf OnListCompleted
End Sub
Private Sub OnListCompleted(ByVal sender As Object, ByVal args As ListCompletedEventArgs)
'...
End Sub
'...
End Class
You mentioned doing processing on a background thread. I'm not completely sure which thread the completion event would issue into, but beware that UI stuff can only happen on the UI thread so you might need to use a Dispatcher.Invoke to make sure your code runs on the right thread. I'd do it to run the RaiseEvent so the view doesn't need to know anything about it.

What is the correct Xaml syntax (and underlying vb code) to have a event raised by a custom Control handled in a host application view model

I have a wpf Custom Control which is now raising custom events. In a test project that I have created to test the ongoing development of the control I have placed a copy of my new custom control on the main window.
In the properties dialog for said control I can see my new event and I can type in the name for a handler for it in the code behind the main window and it's not only created for me but when I place a break point on it I see both the sender and my own event args raised in the control.
What I would now like to know Is what is the correct way to alert a viewmodel about that event respecting the principles of MVVM.
Below is the xaml markup for my custom control in the mainwindow of the test application
<WpfControls:VtlDataNavigator Name="vtlDn"
ItemsSource="{Binding Path=Orders}"
Grid.Row="1"
AddButtonVisibility="True"
CancelNewRecordButtonVisibility="True"
SaveButtonVisibility="True" />
The event I want to catch is called 'RecordControlButtonClicked' and this is the event automatically created in the mainwindow code behind
Private Sub MyCustomHandlerForControlEvent(sender As Object, e As ViewToLearn.WpfControls.VtlDataNavigatorEventArgs) Handles vtlDn.RecordControlButtonClicked
End Sub
So now what I'm after is the correct syntax in both the xaml and my new MainWindowViewModel to have this event transferred across to the viewmodel for handling (or suggestions as to the most efficient way to do this idf there is no direct route.
FYI I already have a relay command set up in the test application but the way that I've used it to bind commands in the past doesn't appear to be working with events.
Edit
One approach I've tried which appears to work is to add the following in the code behind of the main window's loaded event.
Class MainWindow
Public Sub New()
InitializeComponent
DataContext = New MainWindowViewModel
End Sub
Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
Dim mvm As MainWindowViewModel = DirectCast(DataContext, MainWindowViewModel)
mvm.vtldn = vtlDn
End Sub
End Class
and then add the following in the viewmodel;
Private _vtldn As ViewToLearn.WpfControls.VtlDataNavigator
Public Property vtldn As ViewToLearn.WpfControls.VtlDataNavigator
Get
Return _vtldn
End Get
Set(ByVal Value As ViewToLearn.WpfControls.VtlDataNavigator)
If (_vtldn Is Value) Then Return
If Not IsNothing(vtldn) Then
RemoveHandler vtldn.RecordControlButtonClicked, AddressOf MyCustomHandler
End If
_vtldn = Value
If Not IsNothing(vtldn) Then
AddHandler vtldn.RecordControlButtonClicked, AddressOf MyCustomHandler
End If
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(vtldn)))
End Set
End Property
Private Sub MyCustomHandler(ByVal sender As Object, ByVal e As VtlDataNavigatorEventArgs)
'this does get called when buttons in the custom control are clicked
End Sub
However I'm acutely aware that this is not really 'pure' mvvm so if there are better ways to do this I'm open to suggestions.

WPF binded Listbox bug when display 2 items

I have a WPF listbox in my window. In the Load event of the window, i create a List(of Object) and I added some items. At application starts or debug, I can see items.
If I add 1 item on the list, i correctly see 1 only item. If I add 3 or more items, i correctly see 3 or more items. If I add 2 only items, i see 1 only item. Why?
Here is my WPF code
<Window x:Class="Cacatua.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Cacatua;assembly=" >
<Grid>
<ListBox Name="lbSearch" ItemsSource="{Binding}" />
</Grid>
</Window>
And here is my code-behind (same assembly, in Cacatua namespace):
Private myLstSearch As List(Of Object)
Private Sub Window_Loaded(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
myLstSearch = New List(Of Object)
lbSearch.ItemsSource = myLstSearch
Dim myMedia1 as Media1
myMedia1 = New Media1("IdMedia1-A")
myLstSearch.Add(myMedia1)
myMedia1 = New Media1("IdMedia1-B")
myLstSearch.Add(myMedia1)
End Sub
where Media1 is a simple class that contains a string
Public Class Media1
Private myIdTitolo As String
Public ReadOnly Property IDTitolo As String
Get
Return (myIdTitolo)
End Get
End Property
Public Sub New(str As String)
myIdTitolo = str
End Sub
End Class
With this code, I would see a list with this output (there is no datatemplate):
Cacatua.Media1
Cacatua.Media1
but I see only
Cacatua.Media1
I think it's a bug. But am I the first with this problem?
You've got the right idea, but the problem is your ItemsSource doesn't know when to update since you're not using an ObservableCollection. Also there is a timing issue between rendering and loading the window, and I think this has to do with the fact you aren't properly binding your items source.
For starters, try changing the type of myLstSearch to ObservableCollection(Of Media1).
Also, a better way to do this would be to databind it from the XAML directly, so your code-behind would be something like:
Public property MyListSearch As ObservableCollection(Of Media1)
Then your XAML would look like:
<Window x:Class="Cacatua.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Cacatua;assembly=">
<Grid>
<ListBox Name="lbSearch" ItemsSource="{Binding Path=MyListSearch}" />
</Grid>
</Window>
That way, you can simply initialize MyListSearch in your window constructor, and then add elements to it whenever, while your view will automatically update.

how I can get direct access of control placed in User control?

I have a label under my user control. Below is the XAML code of my User Control.
<UserControl x:Class="Class_CalcProject"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ViewModel="clr-namespace:Class_WPFCalc_Project.WPF_Calculator.ViewModelCalculator"
xmlns:local="clr-namespace:Class_WPFCalc_Project"
mc:Ignorable="d"
d:DesignHeight="315" d:DesignWidth="650"
Loaded="SetTextBoxFocus"
KeyUp="KeyBoardHelper">
<Grid>
<Label x:Name="lblHello" />
</Grid>
</User Control>
Below is the Code behind file of User Control in VB.NET:
Import System.Data
Import System.ComponentModel
Import System.Collections
Import System.Collections.ObjectModel
Class Class_CalcProject
Public Sub New()
InitializeComponent()
End Sub
End Class
I have another Class File with Name Testing.vb
Import System.Data
Import System.ComponentModel
Import System.Collections
Import System.Collections.ObjectModel
Class Class_Testing
End Class
My question is Is there any way to access my Label1 (Control) which is placed inside User Control in Testing Class? Where I can directly Change /set any property of Label1. Something similar like:
Label1.IsEnabled = True
or
UserControl.Current.Windows.OfType(Of Class_CalcProject)().First().Label1.IsEnabled = True
In norwal WPF application I can able to access my label control in Testing Class by writing and change / set its property:
Application.Current.Windows.OfType(Of Class_CalcProject)().First().Label1.IsEnabled = True
In windows WPF application the above code is working fine for me. But when I try to convert from Windows to User Control it doesn't work. As I am newbie in WPF and vb.net. Any help in this regard will be highly appreciated. Thanks in advance for posting your reply.
This is what Properties are for. You shouldn't be able to access those controls outside the UserControl directly. But you can.
Any Control in .Net has a property called Controls, which lists the controls immediately inside the parent. You can get them all with some recursion in an extension method:
<Extension()> _
Public Function ChildControls(Of T As Control)(ByVal parent As Control) As List(Of T)
Dim result As New List(Of Control)
For Each ctrl As Control In parent.Controls
If TypeOf ctrl Is T Then result.Add(ctrl)
result.AddRange(ctrl.ChildControls(Of T)())
Next
Return result.ToArray().Select(Of T)(Function(arg1) CType(arg1, T)).ToList()
End Function
This method returns all children controls from the parent. From here, you would need to get the control you are looking for by name. Usage:
Dim myLabel = CType(TeradiodeTestPanel1.ChildControls.Where(Function(c) c.Name = "Label1").Single(), Label)
The Single method will throw an exception if the control isn't found, so Try...Catch accordingly.
You need to set the DataContext class for your UserControl and bind it's various properties to properties in that DataContext class.
It is considered best practice to observe the MVVM pattern and make your ViewModel class the DataContext of your UserControl. Your Testing Class could be that VM it would just need some modifications...
It is also best practice to use Data Binding instead of accessing the control properties in code behind so instead of Label1.IsEnabled = True you'd have <Label x:Name="lblHello" IsEnabled={Binding HelloLabelEnabled}/> in your XAML and HelloLabelEnabled property in your ViewModel which implements some form of change notification...
That's how things are supposed to work in WPF... you could, in theory, continue to code all this in your code-behind but that is highly discouraged and will make your life miserable in the long run...

WPF and VB.net: Data Binding to Separate Class created outside of Expression Blend

I have a WPF application with form that has a textbox named "txtStatusWindow". I also have a vb.net class handed to me by a co-worker that needs to be called and executed by the code in my application code-behind. My co-worker insists that I will need to use common .net events to update the textbox on my form.
The separate vb.net class:
Public Class globalclass
Public Event txtStatusWindow(ByVal Text As String)
Public Sub InitializeProgram()
RaiseEvent txtStatusWindow("Updating something.")
System.Threading.Thread.Sleep(2000)
RaiseEvent txtStatusWindow("Updating something else.")
System.Threading.Thread.Sleep(2000)
RaiseEvent txtStatusWindow("Updating something other than else.")
System.Threading.Thread.Sleep(2000)
RaiseEvent txtStatusWindow("Updating something other than the else stuff.")
System.Threading.Thread.Sleep(2000)
End Sub
End Class
I need to be able to call the sub "InitializeProgram()" from my code-behind, and it needs to be able to update "txtStatusWindow.text" as it runs.
I told him that the updating of the text box can be done with data-binding, but I don't know how to integrate a separate class like this into my project, how to call methods in it, or how to cause it to update my text blocks through data binding.
I also suggested that the methods in this class aren't optimal for connecting to the WPF project anyway, but he just wrote it as an example to discover how to connect the two projects.
Eventually, I will need to integrate classes like these that will be running separate threads to update their data from a dynamic source, and cause many controls to update in my application.
So far, the only way we have been able to get this to work from my code-behind is this:
Partial Public Class SplashScreen
Dim NewText as String
Public WithEvents Globals As globalclass = New globalclass
Public Delegate Sub StringDelegate(ByVal Text As String)
Public SplashText As String
Public Sub New()
MyBase.New()
Me.InitializeComponent()
Me.Show()
Globals.InitializeProgram()
End Sub
Public Sub UpdateSplashscreenHandler(ByVal Text As String) Handles Globals.UpdateSplashScreen
StatusWindowText.Text = Text
End Sub
Notwithstanding the fact that the WPF screen "freezes" until the "globalclass InitializeProgram" method completes (txtStatusWindow.Text does not update while sub without using the esoteric "refresh" extension...), I fully believe we are going about this the wrong way.
There are precious few examples out there concerning the integration and then binding to objects in existing code. Thanks for examining our little quandary.
If this status window is in XAML and the status window is a UserControl, then add a StatusText dependency property to the status window. Then, in the XAML you can bind to the value of that property with something like:
<UserControl x:Name="MyStatusWindow" ...>
<TextBlock Text="{Binding Path=StatusText, ElementName=MyStatusWindow}" />
</UserControl>
Then, from your event, just update the value of that StatusText property.
(Is that even close to what you were asking?)
Also, about that freezing: Instead of doing that updating in the constructor of that class, you might want to do it from the Loaded event of that control. It will still be freezing, though, unless you move it to a separate thread. Right now, that's happening on the same thread that the UI message pump is running on. This is the Dispatcher for that UI.

Resources