Search code examples
c#wpfmvvmcaliburn.microblend

Why do Blend Interaction event triggers fire multiple times when using a Caliburn Micro Conductor.OneActive?


[Note that I asked the wrong question on my first stab at diagnosing this - now corrected.]

I have a WPF application with one main window that inherits from Conductor.Collection.OneActive. It handles navigation requests and keeps a cache of the viewmodels so that the state is maintained. This private collection is almost identical to the base.Items collection but not all of the viewmodels are IScreen.

Everything works fine and state is maintained when we move from one active item to another. However, there is a bug with the Interaction triggers. When the active item is an IScreen, the triggers fire an extra time per navigation, as if they are being wired up again every time; normal triggers do not do this, only the ones from the Interaction library. If the active item is not an IScreen - just inherits from PropertyChangedBase - we do not see this problem, but also we lose the state of the view during navigation.

If you navigate to a view four times, the events will fire four times, five times, five, and so on.

This seems the same as this question but I cannot use his solution since I do not know what the specific viewmodels are and cannot create public properties for them.

My main window class looks like this:

public sealed class MainWindowViewModel : Conductor<object>.Collection.OneActive, IHandle<NavigateToUriMessage>
{
    public void Handle(NavigateToUriMessage message)
    {
        var ignoredUris = RibbonUri.GetItems().Where(t => t.SubTabs.Count > 0).Select(t => t.Uri);

        Func<string, bool> isMatch = uri => uri == message.Uri;

        if (isMatch(RibbonUri.BookkeepingBatchView.Uri))
            GetAndActivateViewModel<BatchPanelViewModel>(message);           
        ...
    }

    private T GetAndActivateViewModel<T>(NavigateToUriMessage message) where T : class
    {
        var vm = GetViewModel<T>(message.Uri);
        ActivateItem(vm);
        return (T) vm;
    }

    private object GetViewModel<T>(string uri) where T : class
    {
        if (viewModelCache.ContainsKey(uri))
            return viewModelCache[uri];
        var vm = viewModelFactory.Create<T>();
        viewModelCache.Add(uri, vm);
        return vm;
    }
}

My MainWindow XAML looks like this:

<Window x:Class="Ui.Views.MainWindowView"
      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" 
      d:DataContext="{d:DesignInstance Type=viewModels:MainWindowViewModel, IsDesignTimeCreatable=False}"
      mc:Ignorable="d"
      WindowState="Maximized" >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="1"/>
            <RowDefinition/>
            <RowDefinition Height="1"/>
            <RowDefinition Height="25"/>
        </Grid.RowDefinitions>
        <ContentControl x:Name="MainRibbon" Grid.Row="0" Margin="0" />
        <Grid Background="#FFBBBBBB" Grid.Row="1" />
        <ContentControl Grid.Row="2" x:Name="ActiveItem" Background="White" />
        <Grid  Grid.Row="3" Background="#FFBBBBBB"/>
        <ContentControl x:Name="BottomStatusBar" Grid.Row="4"/>
    </Grid>
</Window>

And the XAML in the active item view looks like this:

<DataGrid Focusable="True" FocusVisualStyle="{x:Null}" SelectionMode="Single" HeadersVisibility="None" IsReadOnly="True" GridLinesVisibility="None" AutoGenerateColumns="False" Background="Transparent" BorderThickness="0"  Grid.Row="3" Grid.RowSpan="2">
            <i:Interaction.Triggers>
                <ei:KeyTrigger Key="Left" FiredOn="KeyUp" ActiveOnFocus="True" >
                    <cal:ActionMessage MethodName="TryCollapseSelectedItem"/>
                </ei:KeyTrigger>
                <ei:KeyTrigger Key="Right" FiredOn="KeyUp" ActiveOnFocus="True" >
                    <cal:ActionMessage MethodName="TryExpandSelectedItem"/>
                </ei:KeyTrigger>
            </i:Interaction.Triggers>
 ....

Solution

  • It seems that this question here has a better answer, albeit hidden in the comments to the accepted answer. The KeyTrigger class is problematical and events are attached every time the child control is reloaded into the ContentControl. @gunter says, "The real problem seems to be the hook on OnLoaded and that elements on tab controls get multiple OnLoaded events." Then @dain says, "Ok, I see. Well, KeyTrigger is a public class, so you can extend it and override OnEvent to prevent multiple event handler attachment?"

    So I implemented @dain's suggestion, overrode the KeyTrigger class and stopped the event being wired up multiple times:

    public class MyKeyTrigger : KeyTrigger
    {
        private bool eventAttached;
    
        protected override void OnEvent(EventArgs eventArgs)
        {
            if (eventAttached) return;
            base.OnEvent(eventArgs);
            eventAttached = true;
        }
    
        protected override void OnDetaching()
        {
            eventAttached = false;
            base.OnDetaching();
        }