0

I have a SettingsWindow, in it there is an audio file selector which has a context menu. Some code accesses the MyAudioFileSelector computed property before it can get the AudioFileSelector because the AudioFileSelector is just inside a DataTemplate of an item in an ItemsControl that has not yet generated its containers at that moment. I tried to defer the access to MyAudioFileSelector using Dispatcher.BeginInvoke with DispatcherPrority.Loaded, but the item containers are still not yet generated at that moment.

The code that accesses the MyAudioFileSelector is the method that applies one of the many settings inside the user-selected data file. This method is called from the Window's Loaded event handler synchronously for each setting in the program's data files' schema.

I am very new to async-await programming, I have read this but I am not sure how this helps me, and I read this page but I am still not sure what to do. I have read this a but the only answer, unaccepted, seems similar to what I already use below:

MySettingsWindow.Dispatcher.BeginInvoke(new Action(() =>
{
    [...]
}), System.Windows.Threading.DispatcherPriority.Loaded);

A part of the XAML

(InverseBooleanConv just makes true, false, and false, true)

<ItemsControl Grid.ColumnSpan="3" Margin="0,0,-0.6,0" Grid.Row="0"
ItemsSource="{Binding SettingsVMs}" x:Name="MyItemsControl">
    <ItemsControl.Resources>
        <xceed:InverseBoolConverter x:Key="InverseBooleanConv"/>
        <DataTemplate DataType="{x:Type local:AudioFileSettingDataVM}">
            <local:AudioFileSelector MaxHeight="25" Margin="10" FilePath="{Binding EditedValue, Mode=TwoWay}">
                <local:AudioFileSelector.RecentAudioFilesContextMenu>
                    <local:RecentAudioFilesContextMenu
                        PathValidationRequested="RecentAudioFilesContextMenu_PathValidationRequested"
                        StoragePropertyName="RecentAudioFilePaths"
                        EmptyLabel="No recent audio files."/>
                </local:AudioFileSelector.RecentAudioFilesContextMenu>
            </local:AudioFileSelector>
        </DataTemplate>
        [...]

Parts of the code-behind

In MainWindow.xaml.cs, the beginning of the Window_Loaded handler

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    VM.ClockVMCollection.Model.FiltersVM.Init();
    VM.Settings.IsUnsavedLocked = true;
    VM.ClockVMCollection.Model.IsUnsavedLocked = true;
    foreach (KeyValuePair<string, SettingDataM> k in VM.Settings)
    {
        ApplySetting(k.Value);
    }
    [...]

In MainWindow.xaml.cs, in the method ApplySetting

case "AlwaysMute":
    VM.MultiAudioPlayer.Mute = (bool)VM.Settings.GetValue("AlwaysMute");
    break;
case "RecentAudioFilePaths":
    MySettingsWindow.Dispatcher.BeginInvoke(new Action(() =>
    {
        MySettingsWindow.MyRecentAudioFilesContextMenu. // here, MyRecentAudioFilesContextMenu is null, this is the problem
            LoadRecentPathsFromString(VM.Settings.GetValue("RecentAudioFilePaths") as string);
    }), System.Windows.Threading.DispatcherPriority.Loaded);
    break;
case "RecentImageFilePaths":
    MySettingsWindow.Dispatcher.BeginInvoke(new Action(() =>
    {
        MySettingsWindow.MyRecentImageFilesContextMenu. // here, MyRecentImageFilesContextMenu is null, this is the problem
            LoadRecentPathsFromString(
                VM.Settings.GetValue("RecentImageFilePaths") as string);
    }), System.Windows.Threading.DispatcherPriority.Loaded);
    break;
    [...]

In the SettingsWindow class

internal AudioFileSelector MyAudioFileSelector
{
    get
    {
        foreach (SettingDataVM vm in MyItemsControl.ItemsSource)
        {
            if (vm is AudioFileSettingDataVM)
            {
                return (AudioFileSelector)MyItemsControl.ItemContainerGenerator.ContainerFromItem(vm);
            }
        }
        return null;
    }
}
internal ImageFileSelector MyImageFileSelector
{
    get
    {
        foreach (SettingDataVM vm in MyItemsControl.ItemsSource)
        {
            if (vm is ImageFileSettingDataVM)
            {
                return (ImageFileSelector)MyItemsControl.ItemContainerGenerator.ContainerFromItem(vm);
            }
        }
        return null;
    }
}
internal RecentAudioFilesContextMenu MyRecentAudioFilesContextMenu
{
    get
    {
        return MyAudioFileSelector?.RecentAudioFilesContextMenu;
    }
}
internal RecentFilesContextMenu MyRecentImageFilesContextMenu
{
    get
    {
        return MyImageFileSelector?.RecentImageFilesContextMenu;
    }
}

The bug is in the two C# comments in one of the code snippets above, null reference exceptions.

I think I could attach in the MainWindow a handler to SettingsWindow's ItemsControl's ItemContainerGenerator's StatusChanged event and then continue the initialization of the window, including the loading of all the settings, but I wonder if there is a more orderly/correct way.

Thank you.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
silviubogan
  • 3,343
  • 3
  • 31
  • 57
  • Is `MyRecentImageFilesContextMenu` a property of the window, i.e. does the window itself have a `ContextMenu`? – mm8 Sep 17 '19 at 09:56
  • @mm8 No, it is a property of the `ImageFileSelector`, which is a `Control` inside the `SettingsWindow`. I just needed a way to access it directly. – silviubogan Sep 17 '19 at 09:59
  • Well, there is no more "orderly/correct" way than waiting until the element has been created and the `ContextMenu`property has actually been set. – mm8 Sep 17 '19 at 10:01
  • Pretty much any time you write code references ui directly like that, you'd do better using mvvm. Work with data rather than ui.then you won't have any itemgenerator code to have any null reference problems. – Andy Sep 17 '19 at 11:29
  • @Andy How can I use MVVM if I want to load something from a data file, or save something into a data file? Thank you. – silviubogan Sep 17 '19 at 11:53
  • @Andy I posted a new related question [here](https://stackoverflow.com/q/57974279/258462). Thank you. – silviubogan Sep 17 '19 at 12:33

1 Answers1

1

If you have access to your ItemsControl in the code-behind under the variable name MyItemsControl, then you can add an event handler for the ContainerGenerator StatusChanged event:

private void Window_Loaded(object sender, RoutedEventArgs e) {
    //Subscribe to generated containers event of the ItemsControl
    MyItemsControl.ItemContainerGenerator.StatusChanged += ContainerGenerator_StatusChanged;
}

/// <summary>
/// Handles changed in container generator status.
///</summary>
private void ContainerGenerator_StatusChanged(object sender, EventArgs e) {
    var generator = sender as ItemContainerGenerator;
    //Check that containers have been generated
    if (generator.Status == GeneratorStatus.ContainersGenerated ) {
        //Do stuff
    }
}

I really recommand not to use this if what you're after is simply save/load data from a file, as they are completely unrelated.

Corentin Pane
  • 4,794
  • 1
  • 12
  • 29