Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NativeMenu binding for "About" on MacOS not working? #8013

Closed
josephnarai opened this issue Apr 19, 2022 · 24 comments
Closed

NativeMenu binding for "About" on MacOS not working? #8013

josephnarai opened this issue Apr 19, 2022 · 24 comments
Labels

Comments

@josephnarai
Copy link

josephnarai commented Apr 19, 2022

I'm trying to update the About window in the MacOS version of my application.

Following this post:

#3541

The problem is that I can't work out how to bind the AboutCommand

<NativeMenu.Menu>
    <NativeMenu>
      <NativeMenuItem Header="About My App" Command="{Binding AboutCommand}" />
    </NativeMenu>
  </NativeMenu.Menu>

So this is placed in my App.axaml file and then I've added

            AboutCommand = MiniCommand.CreateFromTask(async () =>
            {
                AboutDialogWindow = new AboutDialog();
                var mainWindow = (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
                await AboutDialogWindow.ShowDialog(mainWindow);
            });

in the Initialize() of App.axaml.cs but I get

[Binding] Error in binding to 'Avalonia.Controls.NativeMenuItem'.'Command': 'Null value in expression ''.' (NativeMenuItem #6480969)

I do this and the menu is correctly display "About My App" - but it's greyed out.

I've also tried placing the Native Menu in the MainWindow.axaml file, but that doesn't override it (the default Avalonia About box appears).

I'm obviously just misunderstanding how to implement this. Any advice would be greatly appreciated.

@timunie
Copy link
Contributor

timunie commented Apr 19, 2022

Sounds like a wrong Datacontext to me. Maybe tr to use CompiledBindings to debug you Bindings.

Or use a static command instead. You could access it with x:Static. But I don't know if that is considered as bad practice.

Happy coding
Tim

@timunie
Copy link
Contributor

timunie commented Apr 20, 2022

@josephnarai
Copy link
Author

Yes but from that link:

So you can't use compiled bindings for a command :(

@josephnarai
Copy link
Author

Is there a way to just replace the menu item with an OnClick event or something that does not require binding?

I've tried without success, but thought it's worth asking

@timunie
Copy link
Contributor

timunie commented Apr 20, 2022

Yes but from that link:

So you can't use compiled bindings for a command :(

You can for ICommand as you have it. You can't use it for binding to methods.

@josephnarai
Copy link
Author

josephnarai commented Apr 20, 2022

Unfortunately it doesn't seem to help, it needs a data type and when I specify it it says it can't find it, so I'm obviously missing something. Probably because I've just used a code behind model for my project, so maybe there is view model code I've not setup correctly.

As I asked, do you know if there is a way to do it without binding? I just need it to open a window - an on click code behind would be simple, but I can't get that to work either! I can't use x:Name or Click or Clicked... it doesn't recognise any of those for the NativeMenuItem

@timunie
Copy link
Contributor

timunie commented Apr 20, 2022

Yes you need a ViewModel set as DataContext for Binding. Or you use x:Static instead of Binding.

@josephnarai
Copy link
Author

do you know the syntac for x:Static? I've not found any articles that show an example

@timunie
Copy link
Contributor

timunie commented Apr 20, 2022

do you know the syntac for x:Static? I've not found any articles that show an example

It's for WPF but should work similar

https://docs.microsoft.com/en-us/dotnet/desktop/xaml-services/xstatic-markup-extension

@josephnarai
Copy link
Author

josephnarai commented Apr 20, 2022

So something like

  <NativeMenuItem Header="About DMeter" Command="{Binding {x:Static StaticClassName.AboutCommand}}" />

?

@josephnarai
Copy link
Author

I thought I'd go back and make the example ToDo app and see if I can get the About box to work there... but downloading and building that with .net5.0 as a target - it can't bind to it's content

todo-tutorial-master/Todo/Views/MainWindow.xaml(17,17): Error: XLS0505: Type 'Binding' is used like a markup extension but does not derive from MarkupExtension.

So I think I'll just remove the about box for now.

This seems like something that is far more complicated than it should be.

@timunie
Copy link
Contributor

timunie commented Apr 20, 2022

Note that AboutCommand had to be static.

@josephnarai
Copy link
Author

Yes, I've made a static class, but I can't get the compiler to find it :(

@josephnarai
Copy link
Author

Got it to find it:


<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:DApps.Data"
             x:Class="DApps.App">

  <NativeMenu.Menu>
    <NativeMenu>
      <NativeMenuItem Header="About DMeter" Command="{Binding {x:Static local:NativeMenuModel.AboutCommand}}"/>
    </NativeMenu>
  </NativeMenu.Menu>

but now it's not the correct type:

Avalonia error XAMLIL: Unable to convert DMeterMac:DApps.Helpers.MiniCommand to System.Runtime:System.String for constructor of Avalonia.Markup.Xaml:Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension
It wants a string?

@timunie
Copy link
Contributor

timunie commented Apr 20, 2022

Remove the Binding as i wrote above.

<NativeMenuItem Header="About DMeter" Command="{x:Static local:NativeMenuModel.AboutCommand}" />

@josephnarai
Copy link
Author

josephnarai commented Apr 20, 2022

Oh, I didn't realize you could do it like that. So it's not binding?

That now throws a different exception - so it's progress!

"The type initializer for 'DApps.Data.NativeMenuModel' threw an exception."

namespace DApps.Data
{
    public static class NativeMenuModel
    {
        public static MiniCommand AboutCommand { get; private set; }.  <<<< this is where the exception is thrown

but it seems unrelated... it's System.Collections.Generic.KeyNotFoundException
"Static resource 'Background2' not found.

Which seems totally unrelated?

Oh - it seems it's trying to load the new AboutDialog but can't find the static resources...

Using the static class seems very dodgy....

@timunie
Copy link
Contributor

timunie commented Apr 20, 2022

That's a different issue inside you Dialog.

I would like to close this issue as it's no bug.

For questions like these there is telegram, gitter, discord and the discussion section.

@josephnarai
Copy link
Author

If you say so.

I'm not convinced the static function is the correct way to get this to work. I would suggest a simple example of how to override the default Avalonia About box would be very useful for any mac developers.

@josephnarai
Copy link
Author

josephnarai commented Apr 20, 2022

I went back to see if I could work out if I could get the Binding to work.

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:DApps"
             xmlns:vm="using:DApps"
             x:DataType="vm:App"
             x:Class="DApps.App">

  <Application.Name>DMeter</Application.Name>

  <NativeMenu.Menu>
    <NativeMenu>
      <NativeMenuItem Header="About DMeter" Command="{CompiledBinding AboutCommand}"/>
    </NativeMenu>
  </NativeMenu.Menu>

I put CompiledBinding in and this compiles without error and the App class has the public AboutCommand


 public class App : Application
    {
        public MiniCommand AboutCommand { get; private set; }
        public Window AboutDialogWindow { get; private set; }

        public override void Initialize()
        {
            AvaloniaXamlLoader.Load(this);

            AboutDialogWindow = new AboutDialog();

            AboutCommand = MiniCommand.CreateFromTask(async () =>
            {
                Debug.WriteLine("HERE");
                var mainWindow = (Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
                await AboutDialogWindow.ShowDialog(null);
            });

        }

Yet this still results in

[Binding] Error in binding to 'Avalonia.Controls.NativeMenuItem'.'Command': 'Null value in expression ''.' (NativeMenuItem #6480969)

So I'm not convinced this isn't a bug

@timunie
Copy link
Contributor

timunie commented Apr 20, 2022

You again have no DataContext set. This is not how MVVM works.

@timunie
Copy link
Contributor

timunie commented Apr 20, 2022

I still think a static command would be ok in your case. If not, you need to set somehow your App as the DataContext of your Menu and I don't really see how this should work.

@josephnarai
Copy link
Author

josephnarai commented Apr 22, 2022

Ok, the issue can now be closed. I've finally figured out all the parts that need to be implemented. I'll include sample code here so that if anyone else is having difficulty they have a reference. I still feel that this is overly complicated to replace the AboutAvalonia native menu item and there should be a simpler way to achieve this in a non MVVM application.

Firstly, create a ViewModelBase class:

using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace DApps.ViewModels
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
        {
            if (!EqualityComparer<T>.Default.Equals(field, value))
            {
                field = value;
                RaisePropertyChanged(propertyName);
                return true;
            }
            return false;
        }

        protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Then create an AppViewModel class with your AboutCommand public property

using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using DApps.Helpers;
using DApps.Views;

namespace DApps.ViewModels
{
    public class AppViewModel : ViewModelBase
    {
        public MiniCommand AboutCommand { get; }

        public AppViewModel()
        {
            AboutCommand = MiniCommand.CreateFromTask(async () =>
            {
                AboutDialog dialog = new();
                Avalonia.Controls.Window mainWindow = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
                await dialog.ShowDialog(mainWindow);
            });
        }
    }
}

Create your AboutDialog.axaml

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MyApp.UI"
        MaxWidth="400"
        MaxHeight="475"
        MinWidth="430"
        MinHeight="475"
        Title="About My App"
        Background="{StaticResource Background2}"
        x:Class="MyApp.Views.AboutDialog">
  <Grid Background="{StaticResource Background2}">
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
      <StackPanel Orientation="Vertical" VerticalAlignment="Center">
        <Rectangle Margin="10" Height="1" Fill="{StaticResource Background3}" />
        <TextBlock Text="About" Margin="3" FontSize="14" FontWeight="SemiBold" TextAlignment="Center" Foreground="{StaticResource Foreground2}" />
        <TextBlock x:Name="AboutAppInfo" Text="AppInfo" Margin="1" TextAlignment="Center" Foreground="{StaticResource Foreground2}" />
        </StackPanel>
    </StackPanel>
  </Grid>
</Window>

and code behind class for the about dialog (the code behind can be much simpler than I have here - this is from the code in the avaloina source)

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace DApps.Views
{
    public class AboutDialog : Window
    {
        private static readonly Version s_version = typeof(AboutDialog).Assembly.GetName().Version;

        public static string Version { get; } = s_version.ToString(2);

        public static bool IsDevelopmentBuild { get; } = s_version.Revision == 999;

        public AboutDialog()
        {
            AvaloniaXamlLoader.Load(this);
            DataContext = this;
        }

        public static void OpenBrowser(string url)
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                // If no associated application/json MimeType is found xdg-open opens retrun error
                // but it tries to open it anyway using the console editor (nano, vim, other..)
                ShellExec($"xdg-open {url}", waitForExit: false);
            }
            else
            {
                using Process process = Process.Start(new ProcessStartInfo
                {
                    FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
                    Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "",
                    CreateNoWindow = true,
                    UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
                });
            }
        }

        private static void ShellExec(string cmd, bool waitForExit = true)
        {
            var escapedArgs = cmd.Replace("\"", "\\\"");

            using var process = Process.Start(
                new ProcessStartInfo
                {
                    FileName = "/bin/sh",
                    Arguments = $"-c \"{escapedArgs}\"",
                    RedirectStandardOutput = true,
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    WindowStyle = ProcessWindowStyle.Hidden
                }
            );
            if (waitForExit)
            {
                process.WaitForExit();
            }
        }
    }
}

And finally at the top of App.axaml

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:MyApp"
             xmlns:vm="using:MyApps.ViewModels"
             x:DataType="vm:AppViewModel"
             x:Class="MyApp.App">

  <Application.Name>My App</Application.Name>

  <NativeMenu.Menu>
    <NativeMenu>
      <NativeMenuItem Header="About My App" Command="{CompiledBinding AboutCommand}"/>
    </NativeMenu>
  </NativeMenu.Menu>

and in the code behind App.axaml.cs

using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using DApps.Views;
using DApps.ViewModels;

namespace DApps
{
    public class App : Application
    {
        public App()
        {
            DataContext = new AppViewModel();
        }

        public override void Initialize()
        {
            AvaloniaXamlLoader.Load(this);
        }

        public override void OnFrameworkInitializationCompleted()
        {
            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
            {
                desktop.MainWindow = new MainWindow();
            }

            base.OnFrameworkInitializationCompleted();
        }
    }
}

I hope this might help someone else trying to implement this simple feature.

@majeric
Copy link

majeric commented Jul 19, 2024

MiniMVVM seems to no longer exist. I'm not sure where you're getting MiniCommand from.

@maxkatz6
Copy link
Member

maxkatz6 commented Jul 19, 2024

@majeric it's their command implementation. You are free to choose any MVVM library. Like CommunityToolkit.MVVM or ReactiveUI.

Or just handle Click event without any commands/mvvm.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants