views:

1516

answers:

3

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.

A: 

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();
}
Will Eddins
Thanks for the help, Guard. Unfortunately, it's not simply a matter of taking a long time to populate. It's only about 4000 records, and when I run the above code, VS2008 spins endlessly, consuming hundreds of MBs of RAM, and loading the CPU. However, I will try the delegate approach that you suggest, and will report back.
TimH
This doesn't work. The DataView is owned by the second thread, and I cannot add it to the UI thread. I have tried several ways to marshal it across, without any success. As a last resort, I tried using XamlWriter and XamlReader to get the XAML and add it to the first thread, but XamlReader complains about a root element missing from what XamlWriter created. I can't believe it's this hard to instantiate a user control at runtime!!!!
TimH
Instead of loading the data through the constructor, you could load the data through a seperate method, run that on a seperate thread, and then load the data into the visible control on the main thread. Doing both in the constructor prevents you from doing that.
Will Eddins
I tried doing that, but I couldn't set the datacontext of the usercontrol in the second thread and have it work in the main thread. It kept throwing an exception.
TimH
You cannot access a control from another thread !We all know that when working with threads, we need to be careful about accessing the UI controls. These controls are meant to be accessed only on the thread that built them, which in this case is the primary application thread.
PaN1C_Showt1Me
Here is the link where it is explained: http://www.infosysblogs.com/microsoft/2006/10/cross_thread_ui_control_access.html
PaN1C_Showt1Me
+1  A: 

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.

Richard C. McGuire
Instantiating the DataView from XAML, it loads in about 5 seconds (too long for my purposes, but still, not THAT bad). If I instantiate the DataView from the code behind about, VS2008 never successfully loads it, consumes about 500MB of RAM, and I have to stop debugging because it clearly isn't doing anything after a few minutes. So, I don't think I'm just seeing "it's slow" problems. I think I'm seeing "VS2008 is definitely in an infinite loop and never coming back". Do you believe that using a second thread will still help? Can I really screw the UI doing it this way?
TimH
Thanks Rich. Loading the data in a second thread doesn't work - please see my comment above to Guard, beginning with "This doesn't work."
TimH
A: 

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!

TimH