I'm using MVVMLight to create a questionnaire and running into memory issues when rendering the InkCanvas controls. Here's a watered down example of what I am working with:
QuestionVm
public Question Question { get; set; }
public HandwritingControl HandwritingControl { get; set; }
QuestionnaireVm
public List<QuestionVm> currentQuestions;
public List<QuestionVm> CurrentQuestions
{
get { return currentQuestions; }
set
{
currentQuestions = value;
RaisePropertyChanged();
}
}
Questionnaire.xaml.cs
//Clear form & iterate questions
questionnaireForm.Children.Clear();
foreach (var questionVm in questionnaireVm.CurrentQuestions)
{
questionnaireForm.Children.Add(questionVm.Question);
if(questionVm.HandwritingControl != null)
questionnaireForm.Children.Add(new InkCanvas());
}
The RAM spikes on each page load and it's clear the memory allocated to the InkCanvas is never being deallocated. On the third or so page when roughly ~125 InkCanvas controls are rendered, the app throws a System.OutOfMemoryException.
My question is, why aren't these controls being deallocated? And how can I manually free up the memory? If I comment out the InkCanvas, the questionnaire is fine and Children.Clear() appears to be cleaning up the TextBlocks or any other controls without issue.
UPDATE
So after working with @Grace Feng I tried to refactor my approach and use a ListView with a data template rather than creating a grid from my xaml.cs.
Questionnaire.xaml
<ListView Name="questionnaireListView" ItemsSource="{Binding CurrentQuestions, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Question.Text}" />
<TextBlock Text="{Binding Question.Description}" />
<InkCanvas/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Questionnaire.xaml.cs
private void buttonNext_Click(object sender, RoutedEventArgs e)
{
//Validate & goto next page
if (questionnaireVm.CurrentPageIsValid())
{
questionnaireVm.CurrentQuestions.Clear();
questionnaireVm.LoadNextPage();
}
}
Unfortunately I am still experiencing the same out of memory error even using the ListView data template method. Thoughts?
On the third or so page when roughly ~125 InkCanvas controls are rendered, the app throws a System.OutOfMemoryException.
I just reproduced this problem, and yes you are right about this.
My question is, why aren't these controls being deallocated?
From the code in your Questionnaire.xaml.cs, I think you are dynamic adding questionVm.Question
and a new instance of InkCanvas
to the parent control named "questionnaireForm", and before doing this, you clear the children of this parent control. There is no "deallocating" action during you load your data, so none of these controls will be deallocated.
And how can I manually free up the memory? If I comment out the InkCanvas, the questionnaire is fine and Children.Clear() appears to be cleaning up the TextBlocks or any other controls without issue.
If you manually free up the memory, you will need to remove some of these InkCanvas, or I think what you can do right is for this scenario is using UI virtualization to reduce the memory loss when you load data.
In an UWP APP, there are two controls which have UI virtualization function already, ListView and GridView. I just test these two controls with over 125 instance of empty InkCnavas
. The Item's size of GridView
is adaptive to its layout in the item, so when the InkCanvas
is empty, it will still load all data at once, the out of memory error will still happen. But the ListView
control by default will take one row to hold its items, the UI Virtualization works fine here.
And I saw that you add InkCanvas
based on the questionVm.HandwritingControl != null
, so here is a workaround, you can for example design your Questionnaire.xaml like this:
<Page.Resources>
<DataTemplate x:Key="NoInkCanvasDataTemplate">
<TextBlock Text="{Binding Questions}" />
</DataTemplate>
<DataTemplate x:Key="InkCanvasDataTemplate">
<StackPanel>
<TextBlock Text="{Binding Questions}" />
<InkCanvas></InkCanvas>
</StackPanel>
</DataTemplate>
<local:CustomDataTemplateSelector x:Key="InkCanvasDataTemplateSelector" NoInkCanvas="{StaticResource NoInkCanvasDataTemplate}" InkCanvas="{StaticResource InkCanvasDataTemplate}"></local:CustomDataTemplateSelector>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ListView x:Name="listView" ItemTemplateSelector="{StaticResource InkCanvasDataTemplateSelector}" />
</Grid>
And in the code behind for example:
private ObservableCollection<CurrentQuestions> questions = new ObservableCollection<CurrentQuestions>();
public MainPage()
{
this.InitializeComponent();
listView.ItemsSource = questions;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
questions.Clear();
foreach (var questionVm in questionnaireVm.CurrentQuestions)
{
//Add your data here
}
}
And create a CustomDataTemplateSelector class like this:
public class CustomDataTemplateSelector : DataTemplateSelector
{
public DataTemplate NoInkCanvas
{
get;
set;
}
public DataTemplate InkCanvas
{
get;
set;
}
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
var canvas = item as HandwritingControl;
if (canvas == null)
{
return this.NoInkCanvas;
}
else
{
return InkCanvas;
}
}
}
In a few words, you can do this with a ListView
control and its ItemTemplateSelector
, since I don't have all your code, the above code is just a sample, not 100% correctly for your case.
The problem is in this line
foreach (var questionVm in questionnaireVm.CurrentQuestions)
{
questionnaireForm.Children.Add(questionVm.Question);
if(questionVm.HandwritingControl != null)
questionnaireForm.Children.Add(new InkCanvas()); //everytime you are creating a new object
}
try to create one object of InkCanvas and use it everytime inside the foreach loop. You can create the object in the constructor or at the class level