ModernWindow.MenuLinkGroups - Is there a way to dynamically add links?

Apr 1, 2013 at 4:31 PM
As with the ModernMenu control, is there a way to dynamically add new links to the ModernWindow.MenuLinkGroups?

(I am not sure if this is against CodePlex etiquette, but here is another question unrelated to the title.)
I have checked out a few Metro UI libraries: MahApps, Elysium, and ModernUI. I would say that my favorite is ModernUI. It is a really great library and I thank you for making it. One thing I would like to ask in regards to more features is a notifications feature. In Elysium, there is a NotificationManager that displays a custom message box in the top right of the screen as default due to some designated event. I would like that feature in ModernUI because I would use that as a notification for when the program loses focus and some event happens (similar to the taskbar icon flashing but more noticeable). Are there any plans to release something similar to Elysium's NotificationManager?
Coordinator
Apr 1, 2013 at 5:24 PM
Yes, both LinkGroupCollection and LinkCollection derive from ObservableCollection<T>. Just add your group or link and it automatically appears in the ModernMenu. See also the menu demo in the MUI demo app (controls > modern controls > modernmenu)
Apr 2, 2013 at 1:32 AM
I have been trying to find out how to do it since your reply but I just can't figure it out. I understand how to dynamically add with <mui:ModernMenu> but can't do it with <mui:ModernWindow.MenuLinkGroups>. Didn't want to let my amateurishness show.

I have this as my MainWindow.
<mui:ModernWindow x:Class="ChatWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mui="http://firstfloorsoftware.com/ModernUI"
        WindowStartupLocation="CenterScreen"
        Title="MainWindow" Height="420" Width="550" ContentSource="/Chat.xaml" >

    <mui:ModernWindow.MenuLinkGroups>
        <mui:LinkGroup DisplayName="Chat">
            <mui:LinkGroup.Links>
                <mui:Link DisplayName="Chat" Source="/Chat.xaml" />
                <mui:Link DisplayName="Theme" Source="/Theme.xaml" />
            </mui:LinkGroup.Links>
        </mui:LinkGroup>
    </mui:ModernWindow.MenuLinkGroups>

</mui:ModernWindow>
This is the UserControl for Chat.xaml.
<UserControl x:Class="ChatWPF.Chat"
             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:mui="http://firstfloorsoftware.com/ModernUI"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">

    <Button x:Name="AddLink" Margin="100,6,0,0" VerticalAlignment="Top" HorizontalAlignment="Left" Content="add link" />

</UserControl>
I want to add a link to the <mui:ModernWindow.MenuLinkGroups> of MainWindow when the the Button of Chat is clicked.
The code to add a link to a ModernMenu as in the ModernUIDemo is here.
this.AddLink.Command = new RelayCommand(o => {
                this.Menu.SelectedLinkGroup.Links.Add(new Link {
                    DisplayName = string.Format(CultureInfo.InvariantCulture, "link {0}", ++linkId),
                    Source = new Uri(string.Format(CultureInfo.InvariantCulture, "/link{0}", linkId), UriKind.Relative)
                });
            }, o => this.Menu.SelectedLinkGroup != null);
So far, I have tried naming <mui:ModernWindow.MenuLinkGroups x:Uid="Menu"> and <mui:LinkGroup DisplayName="Chat" x:Name="Menu"> but IntelliSense doesn't find them in the .cs files. I also tried doing something like changing the this.Menu. ... ChatWPF.Chat.Menu. ... but that is also invalid.

I would like to humbly ask how I would make new links for the window menu. I want to add new links to a window link group not a page or usercontrol link group.

Image
Coordinator
Apr 2, 2013 at 7:45 AM
You need to access the ModernWindow instance that is hosting your user control. Then you can access the MenuLinkGroups.
(this is your usercontrol instance)
var wnd = Window.GetWindow(this) as ModernWindow;
if (wnd != null) {
  wnd.MenuLinkGroups.Add(new LinkGroup {
    DisplayName = "new group"
  });
}
Apr 2, 2013 at 1:04 PM
I have the same problem and I tried your snippit, but wnd is Always null in my project.
Coordinator
Apr 2, 2013 at 3:40 PM
Edited Apr 2, 2013 at 3:43 PM
Are you sure your usercontrol is available in the visual tree? If you execute Window.GetWindow before the Load event, null is returned.

Alternatively you can use the following to reference the main window;
var wnd = Application.Current.MainWindow as ModernWindow;
or access all available modern windows;
var wnds = Application.Current.Windows.OfType<ModernWindow>();
Apr 2, 2013 at 5:59 PM
Edited Apr 2, 2013 at 6:10 PM
I was able to add a LinkGroup to the main menu using this code.
private void AddLink_Click(object sender, RoutedEventArgs e)
{
    var wnd = Application.Current.MainWindow as ModernWindow;

    if (wnd != null)
    {
        wnd.MenuLinkGroups.Add(new LinkGroup
        {
            DisplayName = "Chat Control"
        });
    }
}
I want to add a Link to an existing LinkGroup. I am trying to follow the same logic as above but I can't find anything that is valid. Sorry for taking you away from other important matters.

EDIT
Right after posting I found something that satisfies me for the moment. I am not sure if this is the best way to do it.
wnd.MenuLinkGroups.First<LinkGroup>().Links.Add(new Link
{
    DisplayName = "link"
});
Apr 2, 2013 at 6:03 PM
Edited Apr 2, 2013 at 6:07 PM
Are you using the same GroupName for all your links? I made the same mistake and link groups are shown per group name (see here)

And btw. Don't make yourself uncomfortable with asking questions. If we don't ask, we can't learn. There's so much people that are happy to help...
Apr 2, 2013 at 6:29 PM
For my purposes, I have one LinkGroup at the moment and more LinkGroups will be in numerical order. So, I can only think of using the index of the LinkGroup to find the group and then add my Links or whatever.

I understand that if I don't ask I won't ever learn (while I have hit a wall), but then I can't expect everyone to help me figure it out. Especially not the creator of this library who didn't set out to teach but create. Thanks for the encouragement, though.

I would consider my question answered at this point. This discussion can be closed if desired.
Coordinator
Apr 2, 2013 at 10:33 PM
The GroupName property is for grouping link groups, not for uniquely identifying the group itself (as codedevote correctly states). I see that this causes confusion and renaming the GroupName property might be a good idea. Suggestions welcome.
Apr 4, 2013 at 3:54 AM
I have a display prob with creating dynamic menu. So I add two menu link groups, but only the first group links with its sublinks shows, here the code below. Any ideas why ?
    public void BuildUpMenu()
    {
        if ( main == null)
            main = Application.Current.MainWindow as ModernWindow;

        Guard.NotNull<ModernWindow>(() => this.main, main);

        if (main != null)
        {
            main.MenuLinkGroups.Clear();

            // ******************************
            // Build up default menu
            string grpkey = "welcome";

            LinkGroup welcomeGrp = new LinkGroup { GroupName = grpkey, DisplayName = grpkey };
            Link homelnk = new Link { DisplayName = "home", Source = new Uri(@"/View/Home.xaml", UriKind.Relative) };
            Link softlnk = new Link { DisplayName = "settings", Source = new Uri(@"/View/settings.xaml", UriKind.Relative) };
            Link loginlnk = new Link { DisplayName = "login", Source = new Uri(@"/View/login.xaml", UriKind.Relative) };

            welcomeGrp.Links.Add(homelnk);
            welcomeGrp.Links.Add(softlnk);
            welcomeGrp.Links.Add(loginlnk);

            main.MenuLinkGroups.Add(welcomeGrp);

            // ******************************
            // Build up dashboard menu
            grpkey = "dashboard";
            LinkGroup dashboardGrp = new LinkGroup { GroupName = grpkey, DisplayName = grpkey };
            Link consolidatedlnk = new Link { DisplayName = "consolidated", Source = new Uri(@"/View/dashboard.xaml", UriKind.Relative) };

            dashboardGrp.Links.Add(consolidatedlnk);
            main.MenuLinkGroups.Add(dashboardGrp);

            // ******************************
            // Build up title menu
            // ******************************
            main.TitleLinks.Add(softlnk);
            main.TitleLinks.Add(loginlnk);

        }
   }
Apr 4, 2013 at 4:56 AM
ok I think I narrow the problem, I modified your NotifyPropertyChanged.cs to be a bit stricter like the code below, and what I found is at some point when you add the linkgroup, the selected link cannot be set, which throw an exception.

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace FirstFloor.ModernUI.Presentation
{
/// <summary>
/// The base implementation of the INotifyPropertyChanged contract.
/// </summary>
public abstract class NotifyPropertyChanged
    : INotifyPropertyChanged
{
    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;


    /// <summary>
    /// Provides access to the PropertyChanged event handler to derived classes.
    /// </summary>
    protected PropertyChangedEventHandler PropertyChangedHandler
    {
        get
        {
            return PropertyChanged;
        }
    }

    /// <summary>
    /// Verifies that a property name exists in this ViewModel. This method
    /// can be called before the property is used, for instance before
    /// calling RaisePropertyChanged. It avoids errors when a property name
    /// is changed but some places are missed.
    /// <para>This method is only active in DEBUG mode.</para>
    /// </summary>
    /// <param name="propertyName"></param>
    [Conditional("DEBUG")]
    [DebuggerStepThrough]
    public void VerifyPropertyName(string propertyName)
    {
        var myType = this.GetType();

        if (!string.IsNullOrEmpty(propertyName)
            && myType.GetProperty(propertyName) == null)
        {
            var descriptor = this as ICustomTypeDescriptor;

            if (descriptor != null)
            {
                if (descriptor.GetProperties()
                    .Cast<PropertyDescriptor>()
                    .Any(property => property.Name == propertyName))
                {
                    return;
                }
            }
            throw new ArgumentException("Property not found", propertyName);
        }
    }

    /// <summary>
    /// Raises the PropertyChanged event if needed.
    /// </summary>
    /// <remarks>If the propertyName parameter
    /// does not correspond to an existing property on the current class, an
    /// exception is thrown in DEBUG configuration only.</remarks>
    /// <param name="propertyName">The name of the property that
    /// changed.</param>
    [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate",
        Justification = "This cannot be an event")]
    protected virtual void OnPropertyChanged(string propertyName)
    {
        VerifyPropertyName(propertyName);

        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    /// <summary>
    /// Raises the PropertyChanging event if needed.
    /// </summary>
    /// <typeparam name="T">The type of the property that
    /// changes.</typeparam>
    /// <param name="propertyExpression">An expression identifying the property
    /// that changes.</param>
    [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate",
        Justification = "This cannot be an event")]
    [SuppressMessage(
        "Microsoft.Design",
        "CA1006:GenericMethodsShouldProvideTypeParameter",
        Justification = "This syntax is more convenient than other alternatives.")]
    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            var propertyName = GetPropertyName(propertyExpression);
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    /// <summary>
    /// Extracts the name of a property from an expression.
    /// </summary>
    /// <typeparam name="T">The type of the property.</typeparam>
    /// <param name="propertyExpression">An expression returning the property's name.</param>
    /// <returns>The name of the property returned by the expression.</returns>
    /// <exception cref="ArgumentNullException">If the expression is null.</exception>
    /// <exception cref="ArgumentException">If the expression does not represent a property.</exception>
    protected string GetPropertyName<T>(Expression<Func<T>> propertyExpression)
    {
        if (propertyExpression == null)
        {
            throw new ArgumentNullException("propertyExpression");
        }

        var body = propertyExpression.Body as MemberExpression;

        if (body == null)
        {
            throw new ArgumentException("Invalid argument", "propertyExpression");
        }

        var property = body.Member as PropertyInfo;

        if (property == null)
        {
            throw new ArgumentException("Argument is not a property", "propertyExpression");
        }

        return property.Name;
    }




    ///// <summary>
    ///// Raises the PropertyChanged event.
    ///// </summary>
    ///// <param name="propertyName">Name of the property.</param>
    //protected virtual void OnPropertyChanged(string propertyName)
    //{
    //    if (PropertyChanged != null) {
    //        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    //    }
    //}
}
}


In ModernMenu.cs method: UpdateSelection()

ReadOnlyLinkGroupCollection groups = null;
        if (selectedGroup != null) {
            // ensure group itself maintains the selected link
            selectedGroup.SelectedLink = selectedLink;     // throws exception here !!!!

            // find the collection this group belongs to
            var groupName = GetGroupName(selectedGroup);
            this.groupMap.TryGetValue(groupName, out groups);
        }
Coordinator
Apr 4, 2013 at 5:23 PM
You need to make sure the LinkGroup.GroupName property is identical for all link groups that you want to display at once. The GroupName is used for grouping link groups, not for uniquely identifying a group.

This API will change in a future update, it causes confusion.
Apr 4, 2013 at 7:21 PM
Thanks got it to work now, however I still noticed with the changes I made to your NotifyPropertyChanged.cs class, there was a exception throwned in the ModernMenu.cs method: UpdateSelection(). Can I suggest that it would be cleaner to only update the backing store variable of a property and never the property itself, and only raise OnPropertyChanged(() => property) only when the UI needs updating, otherwise updating the UI can become an expensive operation if the controls get bulky. Lastly one more small change to your OnPropertyChanged event to get the correct instance of the event in a multi threaded case.

protected virtual void OnPropertyChanged(string propertyName)
{
    var handler = PropertyChanged;    /// small change
    if (handler != null)
    {
        handler(this, new PropertyChangedEventArgs(propertyName));
    }
}
Coordinator
Apr 6, 2013 at 12:36 PM
Your VerifyPropertyName does not take non-public properties into account. The SelectedLink property is internal and cannot be found by your implementation because it only looks for public properties.
Apr 10, 2013 at 12:38 AM
Edited Apr 10, 2013 at 12:53 AM
Thanks, I seen that. But you are calling OnPropertyChanged() which infers that you giving notification that you want the UI updated. So if you want the UI updated then it has to public and raise OnPropertyChanged(), or if not, then it be a simple property, which is internal without raising OnPropertyChanged(). If you don't do it this way then you could incur a possible performance hit, as updating the UI usually is, especially if someone make it complex, or use that property excessively.

Forgot to add I using your cool Modern UI with MVVMLight, so far so good, just that I need to replace your NotifyPropertyChanged with MVVMLight ViewModelBase, which why I'm having some of these issues. Would like your presentation little bit more decoupled, to implement my mvvm framework of choice.
Coordinator
Apr 10, 2013 at 12:02 PM
Not raising PropertyChanged for non-public properties does make sense. I'll need to check the ramifications.
Apr 10, 2013 at 3:14 PM
I too am using ModernUI with MVVMLight.

Why do you feel like you have to replace mui's NotifyPropertyChanged? I have done nothing of the sort and am using it just fine.
Apr 10, 2013 at 5:03 PM
Using literal strings in NotifyPropertyChanged in this case can allow incorrectly passed property names through. If you use the viewmodelbase of mvvmlight you get strongly typed checking on your property names. There is minimal reflection done, to reflect the property back to its name, and the performance impact is negligible.

I don't know about you but I rather catch problems at compile time instead of runtime.
Apr 10, 2013 at 6:05 PM
Right.

So you are already using ViewModelBase in your VMs.. where is the issue? MUI's implementation of INotifiyPropertyChanged should be of no concern in your VMs? (or am I missing something here?)