In Calculator example, right context menu selection is not working in: OperationsMenuView.xaml
Not sure what's wrong with the XAML here. Must be Avalonia specific since this looks ok from a WPF perspective. Once the options menu pops up on a right click, selecting an item does not fire the CreateOperationCommand in the OperationInfoViewModel.
<Button Content="{Binding Title}"
Command="{Binding DataContext.CreateOperationCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Background="Transparent"
BorderBrush="Transparent"
Foreground="{DynamicResource ForegroundBrush}"
Padding="3"
Cursor="Hand"
HorizontalContentAlignment="Left">
<Button.Theme>
<ControlTheme TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Name="Border"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<ContentPresenter Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style Selector="^:pointerover /template/ Border#Border">
<Setter Property="Background"
Value="{DynamicResource NodeInput.BorderBrush}" />
</Style>
</ControlTheme>
</Button.Theme>
</Button>
Turns out the problem was in the EditorView.xaml.cs code.
For now I just commented out the offending line as shown below:
private void CloseOperationsMenu(object? sender, RoutedEventArgs e)
{
ItemContainer? itemContainer = sender as ItemContainer;
NodifyEditor? editor = sender as NodifyEditor ?? itemContainer?.Editor;
if (!e.Handled && editor?.DataContext is CalculatorViewModel calculator)
{
//MP! Fixed: Operation is called too early, not allowing events to fire on time, besides being duplicated by lower level layer.
//calculator.OperationsMenu.Close();
}
}
The above was not at all obvious due to the duplication of needlessly problematic code logic. This will need to be cleaned up further.
I have noticed a few other little bugs that I'm starting to look into.
Here's the completed fix to obtain correct behavior.
Changes in OperationsMenuView.xaml:
<UserControl x:Class="Nodify.Calculator.OperationsMenuView"
xmlns="https://github.com/avaloniaui"
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:Nodify.Calculator"
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
mc:Ignorable="d"
MinWidth="250"
d:DesignHeight="400"
d:DesignWidth="250"
x:DataType="local:OperationsMenuViewModel"
Bounds="{Binding Bounds,Mode=OneWayToSource}">
<!--
MP! Note: breaks design preview
d:DataContext="{d:DesignInstance local:OperationsMenuViewModel}"
-->
<UserControl.Resources>
<ControlTheme TargetType="{x:Type TextBlock}" x:Key="{x:Type TextBlock}">
<Setter Property="Foreground"
Value="{DynamicResource ForegroundBrush}" />
</ControlTheme>
</UserControl.Resources>
<Border Padding="7"
CornerRadius="3"
Background="{DynamicResource Node.BackgroundBrush}"
BorderBrush="{StaticResource NodifyEditor.SelectionRectangleStrokeBrush}"
BorderThickness="2"
IsVisible="{Binding IsVisible, Converter={shared:BooleanToVisibilityConverter}}">
<!--
MP! Note: Should be IsVisible="{Binding IsVisible}"
BooleanToVisibilityConverter does nothing but reduce code diffing issues.
Though it does result in a trivial performance hit.
-->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!--
MP! Note: Casting may be needed in some scenarios, but not here.
Command="{Binding DataContext.CreateOperationCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
Command="{Binding ((local:OperationsMenuViewModel)DataContext).CreateOperationCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
-->
<ItemsControl Grid.Row="1"
ItemsSource="{Binding AvailableOperations}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:OperationInfoViewModel}">
<Button Content="{Binding Title}"
Command="{Binding DataContext.CreateOperationCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Background="Transparent"
BorderBrush="Transparent"
Foreground="{DynamicResource ForegroundBrush}"
Padding="3"
Cursor="Hand"
HorizontalContentAlignment="Left">
<Button.Theme>
<ControlTheme TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Name="Border"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<ContentPresenter Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style Selector="^:pointerover /template/ Border#Border">
<Setter Property="Background"
Value="{DynamicResource NodeInput.BorderBrush}" />
</Style>
</ControlTheme>
</Button.Theme>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
</UserControl>
Changed in EditorView.xaml.cs:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Nodify.Calculator
{
public partial class EditorView : UserControl
{
// MP! Fix: Context menu handling.
private Size _calcSize;
private Point _openPressPosition;
private Point _closePressPosition;
public EditorView()
{
InitializeComponent();
PointerPressedEvent.AddClassHandler<NodifyEditor>(CloseOperationsMenuPointerPressed, RoutingStrategies.Tunnel);
ItemContainer.DragStartedEvent.AddClassHandler<ItemContainer>(CloseOperationsMenu);
PointerReleasedEvent.AddClassHandler<NodifyEditor>(OpenOperationsMenu);
Editor.AddHandler(DragDrop.DropEvent, OnDropNode);
}
private void OpenOperationsMenu(object? sender, PointerReleasedEventArgs e)
{
if (!e.Handled && e.Source is NodifyEditor editor && !editor.IsPanning && editor.DataContext is CalculatorViewModel calculator &&
e.InitialPressMouseButton == MouseButton.Right)
{
e.Handled = true;
// MP! Fix: Context menu handling.
_openPressPosition = e.GetPosition(this);
calculator.OperationsMenu.OpenAt(editor.MouseLocation);
}
}
private void CloseOperationsMenuPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonPressed)
{
_closePressPosition = e.GetPosition(this);
CloseOperationsMenu(sender, e);
}
}
private void CloseOperationsMenu(object? sender, RoutedEventArgs e)
{
ItemContainer? itemContainer = sender as ItemContainer;
NodifyEditor? editor = sender as NodifyEditor ?? itemContainer?.Editor;
if (!e.Handled && editor?.DataContext is CalculatorViewModel calculator)
{
_calcSize = _calcSize == default ? calculator.OperationsMenu.Bounds.Size : _calcSize;
var calcRectangle = new Rect(_openPressPosition, _calcSize);
//MP! Fixed: Only call Close() if lower layer didn't do it, which only occurs when we click outside the popup menu.
if (!calcRectangle.Contains(_closePressPosition))
{
calculator.OperationsMenu.Close();
}
}
}
private void OnDropNode(object? sender, DragEventArgs e)
{
NodifyEditor? editor = (e.Source as NodifyEditor) ?? (e.Source as Control)?.GetLogicalParent() as NodifyEditor;
if(editor != null && editor.DataContext is CalculatorViewModel calculator
&& e.Data.Get(typeof(OperationInfoViewModel).FullName) is OperationInfoViewModel operation)
{
OperationViewModel op = OperationFactory.GetOperation(operation);
op.Location = editor.GetLocationInsideEditor(e);
calculator.Operations.Add(op);
e.Handled = true;
}
}
private void OnNodeDrag(object? sender, MouseEventArgs e)
{
if(leftButtonPressed && ((Control)sender).DataContext is OperationInfoViewModel operation)
{
var data = new DataObject();
data.Set(typeof(OperationInfoViewModel).FullName, operation);
DragDrop.DoDragDrop(e, data, DragDropEffects.Copy);
}
}
private void OnNodePressed(object? sender, PointerPressedEventArgs e)
{
leftButtonPressed = e.GetCurrentPoint(this).Properties.PointerUpdateKind ==
PointerUpdateKind.LeftButtonPressed;
}
private void OnNodeExited(object? sender, PointerEventArgs e)
{
leftButtonPressed = false;
}
private bool leftButtonPressed;
}
}
Hi, thanks for the fix, can you please make a PR? Later Ill compare the WPF version to see why there is logic difference
This is going to sound lame, but I won't be able to get to this awhile longer. I'm still in the process of sorting out my dev environments. I've been struggling to resolve what to use on the Mac side. I'm not a fan of VS Code. Visual Studio 2022 for Mac is only going to be viable for another year or so. The real problem I have is what to do with all my custom VS extensions that greatly speed up my development.
Microsoft is slowly killing me, hence my desperate attempt to move away from their tech while keeping as much of my XAML/C# bits as possible.
What do you use on the Mac? Has anyone published an Avalonia app to the Mac store?
I should be able to get to PRs in about two weeks. Since this bug fix is in a sample, it's not critical. Nice work by the way.
Lastly, I think it might be possible to coordinate with the WPF author to simplify supporting both WPF and Avalonia from the same sources. Given what Microsoft is doing of late, that probably won't be a hard sell.
Pull request posted sooner than expected as I was able to resolve my setup issues. Please ignore my whining above. Oh and the converter fix allows the other examples to work correctly as far as I can tell.
Thanks for the PR, merged!
Also, personally I use Rider on a Mac and I think it works amazing. And I know people publish Avalonia apps to the App Store and they are accepted.
Also, it is not possible to support both WPF and Avalonia with the same sources. Avalonia, even though is a similar framework, is ultimately completely different code. And pretty much all Nodify code is UI Framework related, a repo like this, which is regularly merged with the upstream, is the best we can have I think.