From 8e2bf4a54a04de2fb9323a761c2850e2b77febad Mon Sep 17 00:00:00 2001 From: Pavel Anpin Date: Mon, 29 Dec 2025 06:39:29 -0300 Subject: [PATCH 1/2] bump flake --- flake.lock | 24 ++++++++++++------------ shell.nix | 2 +- shell_fhs.nix | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/flake.lock b/flake.lock index cb9805a..7c63f33 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1749932438, - "narHash": "sha256-zXHpbiCRv+h9+SLdC0H/6icbQVuTQTuT2wpw3FitCo8=", + "lastModified": 1766953408, + "narHash": "sha256-N7YHIPspxzzb9LTGbggXccwycbJDLVIbZ1bXW8iN8iY=", "owner": "tadfisher", "repo": "android-nixpkgs", - "rev": "04d5b7d7c59f93ebe89b522ff56da4d650ee5c5b", + "rev": "25b06c7a25f822c1e7eed526512c725312ceae8a", "type": "github" }, "original": { @@ -28,11 +28,11 @@ ] }, "locked": { - "lastModified": 1741473158, - "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", + "lastModified": 1764011051, + "narHash": "sha256-M7SZyPZiqZUR/EiiBJnmyUbOi5oE/03tCeFrTiUZchI=", "owner": "numtide", "repo": "devshell", - "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", + "rev": "17ed8d9744ebe70424659b0ef74ad6d41fc87071", "type": "github" }, "original": { @@ -61,11 +61,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1749794982, - "narHash": "sha256-Kh9K4taXbVuaLC0IL+9HcfvxsSUx8dPB5s5weJcc9pc=", + "lastModified": 1766651565, + "narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ee930f9755f58096ac6e8ca94a1887e0534e2d81", + "rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539", "type": "github" }, "original": { @@ -77,11 +77,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1749794982, - "narHash": "sha256-Kh9K4taXbVuaLC0IL+9HcfvxsSUx8dPB5s5weJcc9pc=", + "lastModified": 1766902085, + "narHash": "sha256-coBu0ONtFzlwwVBzmjacUQwj3G+lybcZ1oeNSQkgC0M=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ee930f9755f58096ac6e8ca94a1887e0534e2d81", + "rev": "c0b0e0fddf73fd517c3471e546c0df87a42d53f4", "type": "github" }, "original": { diff --git a/shell.nix b/shell.nix index c0b3001..7c04d7c 100644 --- a/shell.nix +++ b/shell.nix @@ -26,7 +26,7 @@ let aapt llvm_18 zip - nuget-to-nix + nuget-to-json nixpkgs-fmt nil jetbrains.rider diff --git a/shell_fhs.nix b/shell_fhs.nix index 8082908..128674c 100644 --- a/shell_fhs.nix +++ b/shell_fhs.nix @@ -19,7 +19,7 @@ let ]); # FHS environment for installing official .NET SDK - dotnet-fhs = buildFHSUserEnv { + dotnet-fhs = buildFHSEnv { name = "dotnet-fhs"; targetPkgs = pkgs: (with pkgs; [ coreutils @@ -64,7 +64,7 @@ let jdk17 aapt zip - nuget-to-nix + nuget-to-json nixpkgs-fmt nil python3 From b6ee8849926574aa876e100c46e9bb31ad6dbbb0 Mon Sep 17 00:00:00 2001 From: Pavel Anpin Date: Mon, 29 Dec 2025 08:19:18 -0300 Subject: [PATCH 2/2] update collection view sample with per-item menu --- sample/MainPage.xaml | 74 +++++++++----- sample/MainPage.xaml.cs | 4 +- sample/ViewModels/MainViewModel.cs | 158 +++++++++++++++++++++++++++-- sample/run.sh | 27 +++++ 4 files changed, 232 insertions(+), 31 deletions(-) create mode 100755 sample/run.sh diff --git a/sample/MainPage.xaml b/sample/MainPage.xaml index 3083cae..7aa0103 100644 --- a/sample/MainPage.xaml +++ b/sample/MainPage.xaml @@ -8,6 +8,7 @@ Title="ContextMenuContainer Demo" BackgroundColor="#f5f5f5" AutomationId="sample_page" + x:Name="mainPage" > @@ -27,6 +28,32 @@ + + + + + + + + + + + + + + + + @@ -129,32 +156,31 @@ - - diff --git a/sample/MainPage.xaml.cs b/sample/MainPage.xaml.cs index 44d765b..b95799a 100644 --- a/sample/MainPage.xaml.cs +++ b/sample/MainPage.xaml.cs @@ -1,5 +1,5 @@ using Microsoft.Maui.Controls; - +using APES.MAUI.Sample.ViewModels; namespace APES.MAUI.Sample; public partial class MainPage : ContentPage { @@ -7,5 +7,7 @@ public MainPage() { InitializeComponent(); } + public IMainViewModel ViewModel => BindingContext as IMainViewModel; + } diff --git a/sample/ViewModels/MainViewModel.cs b/sample/ViewModels/MainViewModel.cs index f106819..f09edc0 100644 --- a/sample/ViewModels/MainViewModel.cs +++ b/sample/ViewModels/MainViewModel.cs @@ -8,7 +8,9 @@ namespace APES.MAUI.Sample.ViewModels { - public class MainViewModel : INotifyPropertyChanged + + + public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); @@ -20,7 +22,62 @@ protected bool SetField(ref T field, T value, [System.Runtime.CompilerService NotifyPropertyChanged(propertyName); return true; } + } + + /// + /// Represents a person for the CollectionView example. + /// + public class Person : ViewModelBase + { + private string _firstName = string.Empty; + public string FirstName + { + get => _firstName; + set + { + SetField(ref _firstName, value); + NotifyPropertyChanged(nameof(FullName)); + NotifyPropertyChanged(nameof(FirstLetter)); + NotifyPropertyChanged(nameof(AutomationId)); + NotifyPropertyChanged(nameof(NameAutomationId)); + } + } + + private string _lastName = string.Empty; + public string LastName + { + get => _lastName; + set + { + SetField(ref _lastName, value); + NotifyPropertyChanged(nameof(FullName)); + NotifyPropertyChanged(nameof(AutomationId)); + NotifyPropertyChanged(nameof(NameAutomationId)); + } + } + public string FullName => $"{FirstName} {LastName}"; + public string FirstLetter => string.IsNullOrEmpty(FirstName) ? "#" : FirstName[0].ToString().ToUpper(); + public string AutomationId => $"person_{FirstName?.ToLower()}_{LastName?.ToLower()}"; + public string NameAutomationId => $"person_name_{FirstName?.ToLower()}_{LastName?.ToLower()}"; + } + + public interface IMainViewModel { + public ICommand FirstCommand { get; } + public ICommand SecondCommand { get; } + public ICommand DynamicSectionCommand { get; } + public ICommand DestructiveCommand { get; } + public ICommand ConstructiveCommand { get; } + public ICommand NeverEndingCommand { get; } + public ICommand ToggleConditionalCommand { get; } + public ICommand ConditionalCommand { get; } + public ICommand AddItemCommand { get; } + public ICommand ClearItemsCommand { get; } + public ICommand EditPersonCommand { get; } + public ICommand DeletePersonCommand { get; } + } + public class MainViewModel : ViewModelBase, IMainViewModel + { #region Properties private string _text; public string Text @@ -48,6 +105,8 @@ public string DynamicSectionText public ICommand ConditionalCommand { get; } public ICommand AddItemCommand { get; } public ICommand ClearItemsCommand { get; } + public ICommand EditPersonCommand { get; } + public ICommand DeletePersonCommand { get; } #endregion #region Context Menu Properties @@ -72,8 +131,16 @@ public string ConditionalActionStatus set => SetField(ref _conditionalActionStatus, value); } - private ObservableCollection _listItems = new(); - public ObservableCollection ListItems + + private string _collectionViewResultText = "Right-click on a person to see actions"; + public string CollectionViewResultText + { + get => _collectionViewResultText; + set => SetField(ref _collectionViewResultText, value); + } + + private ObservableCollection _listItems = new(); + public ObservableCollection ListItems { get => _listItems; set => SetField(ref _listItems, value); @@ -95,6 +162,8 @@ public MainViewModel() ConditionalCommand = new Command(ConditionalAction); AddItemCommand = new Command(AddItem); ClearItemsCommand = new Command(ClearItems); + EditPersonCommand = new Command(EditPerson); + DeletePersonCommand = new Command(DeletePerson); _deleteIconSource = "outline_delete_black_24.png"; SettingsIconSource = "outline_settings_black_24.png"; @@ -174,11 +243,49 @@ private void ToggleConditionalAction() private void InitializeListItems() { // Add some initial items - ListItems.Add("Example Item 1"); - ListItems.Add("Example Item 2"); + // ListItems.Add(new Person { FirstName = "Thiago", LastName = "Ferreira" }); + // ListItems.Add(new Person { FirstName = "Noor", LastName = "Abdallah" }); + // ListItems.Add(new Person { FirstName = "Mateo", LastName = "Garcia" }); + // ListItems.Add(new Person { FirstName = "Yuki", LastName = "Tanaka" }); + AddItem(); + AddItem(); + } - private void AddItem() => ListItems.Add($"New Item {ListItems.Count + 1}"); + private readonly List<(string First, string Last)> _namePool = new() { + ("Dmitri", "Ivanov"), + ("João", "Silva"), + ("Priya", "Sharma"), + ("Ahmed", "Al-Sayed"), + ("Anastasia", "Volkova"), + ("Pedro", "Oliveira"), + ("Rahul", "Verma"), + ("Fatima", "Hassan"), + ("Ivan", "Sokolov"), + ("Ana", "Costa"), + ("Ananya", "Singh"), + ("Omar", "Khalid"), + ("Olga", "Smirnova"), + ("Maria", "Santos"), + ("Rohan", "Gupta"), + ("Layla", "Ibrahim"), + ("Vladimir", "Popov"), + ("Lucas", "Pereira"), + ("Vikram", "Patel"), + ("Youssef", "Mahmoud") + }; + + private void AddItem() { + var c = ListItems.Count; + (string First, string Last) p; + if(c < _namePool.Count) { + p = _namePool[ListItems.Count]; + } else + { + p = ("New person", $"#{c}"); + } + ListItems.Add(new Person() { FirstName = p.First, LastName = p.Last }); + } private void ClearItems() => ListItems.Clear(); @@ -198,5 +305,44 @@ public long ConditionalCommandCounter public FileImageSource SettingsIconSource { get; private set; } private readonly FileImageSource _deleteIconSource; + + private void EditPerson(Person person) + { + if (person == null){ + CollectionViewResultText = "Person was null!"; + Logger.Error("ContextMenuContainer EditPerson person was null!"); + return; + } + CollectionViewResultText = $"Editing: {person. FullName} at {DateTime.Now:HH: mm:ss}"; + } + + private void DeletePerson(Person person) + { + + + try + { + if (person == null){ + CollectionViewResultText = "Person was null!"; + Logger.Error("ContextMenuContainer DeletePerson person was null!"); + return; + } + var index = ListItems.IndexOf(person); + if (index < 0 ) + { + CollectionViewResultText = "Person was not found!"; + Logger.Error("ContextMenuContainer DeletePerson person return index less than 0!"); + } + ListItems.RemoveAt(index); + CollectionViewResultText = $"Deleted: {person.FullName} at {DateTime.Now:HH:mm:ss}"; + } + catch(Exception ex) + { + CollectionViewResultText = ex.Message; + + } + } + + } } diff --git a/sample/run.sh b/sample/run.sh new file mode 100755 index 0000000..145e50a --- /dev/null +++ b/sample/run.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +TFM="net9.0-android" +CONFIG="Debug" +PKG="com.apes.maui.sample" # <-- change me +LOG="maui-logcat.log" + +adb wait-for-device >/dev/null + +dotnet build -t:Run -f "$TFM" -c "$CONFIG" + +# wait for PID +echo "Waiting for PID of $PKG..." +for _ in {1..60}; do + PID="$(adb shell pidof -s "$PKG" 2>/dev/null | tr -d '\r' || true)" + [[ -n "${PID:-}" ]] && break + sleep 1 +done + +if [[ -z "${PID:-}" ]]; then + echo "PID not found after 60s. Full log: $LOG" + exit 1 +fi + +echo "App PID=$PID (app-only logcat; Ctrl+C to stop)" +adb logcat -v time --pid="$PID"