AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.34k stars 2.2k forks source link

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

Closed josephnarai closed 2 years ago

josephnarai commented 2 years ago

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

Following this post:

https://github.com/AvaloniaUI/Avalonia/issues/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 commented 2 years ago

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 commented 2 years ago

https://docs.avaloniaui.net/docs/data-binding/compiledbindings

josephnarai commented 2 years ago

Yes but from that link:

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

josephnarai commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

josephnarai commented 2 years ago

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

timunie commented 2 years ago

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 commented 2 years ago

So something like

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

?

josephnarai commented 2 years ago

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 commented 2 years ago

Note that AboutCommand had to be static.

josephnarai commented 2 years ago

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

josephnarai commented 2 years ago

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 commented 2 years ago

Remove the Binding as i wrote above.

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

josephnarai commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

timunie commented 2 years ago

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 commented 2 years ago

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 commented 2 months ago

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

maxkatz6 commented 2 months ago

@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.