Search code examples
c#modern-ui

FirstFloor ModernUI Unable to Navigate via TitleLinks.LinkNavigator


I'm trying to navigate to an XAML control (specifically ModernFrame control) via the following code when a keyboard shortcut is pressed:

LinkNavigator.Navigate(new Uri("/Controls/SettingsManager.xaml", UriKind.Relative), this);

The keyboard shortcut fires and then I am given an exception:

System.ArgumentException: 'Unable to navigate to /Controls/SettingsManager.xaml, could not find a ModernFrame target '''

The following is the source for the SettingsManager as a ModernFrame--note that this still works if you change it to a UserControl. I changed it to a ModernFrame because of the aforementioned exception which is looking for a ModernFrame.

Now the SettingsManager.xaml control functions perfectly fine if I navigate to it via a TitleLink within the window. However the moment I try to navigate to it programmatically I receive the exception. You can leave the control completely empty and the exception is still thrown.

SettingsManager.xaml.cs:

using System.Collections.Generic;
using System.Linq;
using System.Windows.Controls;
using FirstFloor.ModernUI.Presentation;
using FirstFloor.ModernUI.Windows.Controls;
using KeystoneEstimating.Containers;

namespace KeystoneEstimating.Controls {
    /// <summary>
    /// Interaction logic for SettingsManager.xaml
    /// </summary>
    public partial class SettingsManager : ModernFrame {
        public SettingsManager() {
            InitializeComponent();
        
            /// Load settings into the Link interface of MUI.
            List<AppSettings> settings = AppInfo.SettingsContainers;
            foreach (AppSettings set in settings) {
                Link lnk = new Link();
                lnk.DisplayName = set.SettingsName;
                lnk.Source = set.ControlPath;
                SettingsLinks.Links.Add(lnk);
            }

            // Load up the very first registered settings page.
            if (SettingsLinks.Links.First() != null)
                SettingsLinks.SelectedSource = SettingsLinks.Links.First().Source;
        }
    }
}

SettingsManage.xaml:

<mui:ModernFrame xmlns:mui="http://firstfloorsoftware.com/ModernUI"
             x:Class="KeystoneEstimating.Controls.SettingsManager"
             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:local="clr-namespace:KeystoneEstimating"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid Style="{StaticResource ContentRoot}">
        <mui:ModernTab Layout="List" Name="SettingsLinks"/>
    </Grid>
</mui:ModernFrame>

ModernUI comes in two NugetPackages labeled ModernUI.WPFCore. One is version 2.0.0 and the other is version 3.1.2. The problem is reproducible on both.


Solution

  • Seems like there is an inherent issue with FirstFloor ModernUI's Navigation command where it cannot find the MainWindow.xaml's ContentFrame which is a style control of type ModernFrame when calling Navigate().

    I searched into the style from the source code and found that through the XAML the ContentFrame is being found via WPF bindings.

    <Button Content="{Binding DisplayName}"
            Command="navigation:LinkCommands.NavigateLink"
            CommandParameter="{Binding Source}"
            CommandTarget="{Binding ElementName=ContentFrame}"
            Style="{StaticResource SystemButtonLink}" />
    

    When you click any Link in a ModernWindow this is how the code is handled and works perfectly when there you've precoded the UI elements in XAML because all XAML elements will automatically inherit the ModernWindow as their parent object. Which is necessary for the Navigate command as it does a search up the element hierarchy of the parent to find a ModernFrame to populate into. See DefaultLinkNavigator.Navigate calls a function NavigationHelper.FindFrame(parameter, source):

        /// <summary>
        /// Finds the frame identified with given name in the specified context.
        /// </summary>
        /// <param name="name">The frame name.</param>
        /// <param name="context">The framework element providing the context for finding a frame.</param>
        /// <returns>The frame or null if the frame could not be found.</returns>
    public static ModernFrame FindFrame(string name, FrameworkElement context)
    {
        if (context == null) {
            throw new ArgumentNullException("context");
        }
    
        // collect all ancestor frames
        var frames = context.AncestorsAndSelf().OfType<ModernFrame>().ToArray();
    
        if (name == null || name == FrameSelf) {
            // find first ancestor frame
            return frames.FirstOrDefault();
        }
        if (name == FrameParent) {
            // find parent frame
            return frames.Skip(1).FirstOrDefault();
        }
        if (name == FrameTop) {
            // find top-most frame
            return frames.LastOrDefault();
        }
    
        // find ancestor frame having a name matching the target
        var frame = frames.FirstOrDefault(f => f.Name == name);
            
        if (frame == null) {
            // find frame in context scope
            frame = context.FindName(name) as ModernFrame;
    
            if (frame == null) {
                // find frame in scope of ancestor frame content
                var parent = frames.FirstOrDefault();
                if (parent != null && parent.Content != null) {
                    var content = parent.Content as FrameworkElement;
                    if (content != null) {
                        frame = content.FindName(name) as ModernFrame;
                    }
                }
            }
        }
    
        return frame;
    }
    

    However this is not the case in code... When executing links in code the XAML page of the control you wish to navigate to doesn't exist yet and as such has no parent and cannot be used to find the parent's ModernFrame on navigation.

    The solution however is to either to create a binding in code and pass the binding as a parameter to the Navigate command--which I don't know how to do yet--or do a hierarchical search down the ModernWindow control and manually search for all source elements of type ModernFrame with the name ContentFrame (which is the display element of the window in the ModernWindow.xaml that shows your content.

    First add a hierarchical search function to your MainWindow.cs class--which should be extending ModernWindow instead of Window in WPF:

    public static T FindChild<T>(DependencyObject parent, string childName)
        where T : DependencyObject {
        // Confirm parent and childName are valid. 
        if (parent == null) return null;
    
        T foundChild = null;
    
        int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < childrenCount; i++) {
            var child = VisualTreeHelper.GetChild(parent, i);
            // If the child is not of the request child type child
            T childType = child as T;
            if (childType == null) {
                // recursively drill down the tree
                foundChild = FindChild<T>(child, childName);
    
                // If the child is found, break so we do not overwrite the found child. 
                if (foundChild != null) break;
            } else if (!string.IsNullOrEmpty(childName)) {
                var frameworkElement = child as FrameworkElement;
                // If the child's name is set for search
                if (frameworkElement != null && frameworkElement.Name == childName) {
                    // if the child's name is of the request name
                    foundChild = (T)child;
                    break;
                }
            } else {
                // child element found.
                foundChild = (T)child;
                break;
            }
        }
    

    Within the same class to navigate to a control via code--in my case when a keybinding is executed--you can use three different methods:

    var target = FindChild<ModernFrame>(this, "ContentFrame");
    // A:
    LinkCommands.NavigateLink.Execute(new Uri("/Controls/SettingsManager.xaml", UriKind.RelativeOrAbsolute), target);
                
    // B:
    NavigationCommands.GoToPage.Execute("/Controls/SettingsManager.xaml", target);
                
    // C:
    this.LinkNavigator.Navigate(new Uri("/Controls/SettingsManager.xaml", UriKind.Relative), target as FrameworkElement);
    

    Source for hierarchy child search: https://stackoverflow.com/a/1759923/2808956