<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      Maui 實踐:為控件動態擴展 DragDrop 能力

      作者:夏群林 原創 2025.6.9

      拖放的實現,和其他的 GestureRecognizer 不同,需要 DragGestureRecognizer 與 DropGestureRecognizer 相互配合,Drag / Drop 又是在不同的控件上附加的,數據傳輸和配置相對復雜,不太好理解。需要徹底閱讀源代碼,才能真的把握。我做了一個擴展方法,把復雜的配置包裹起來,在代碼層面與要附加拖放功能的控件分離,用戶只需關注拖放動作所支持的業務功能即可。

      直接上代碼。

      一、核心架構與關鍵組件

      1. 數據載體:DragDropPayload<TView>

      解耦控件與業務邏輯,封裝拖放所需的視圖引用、附加數據和回調邏輯。

      public interface IDragDropPayload
      {
          public View View { get; }                   // 拖放源/目標控件
          public object? Affix { get; }               // 任意附加數據(如文本、對象)
          public Action? Callback { get; }            // 拖放完成后的回調
      }
      public class DragDropPayload<TView> : IDragDropPayload where TView : View
      {
          public required TView View { get; init; }
          public object? Affix { get; init; }
          public Action? Callback { get; init; }
          View IDragDropPayload.View => View;
      }
      

      關鍵點

      • View:強類型視圖引用,確保拖放操作與具體控件綁定。
      • Affix:支持傳遞復雜數據,用于拖和放時,對源控件和目標控件進行處理所需附加的數據。 默認為 null。
      • Callback:用于執行拖放后的輕量化操作(如日志記錄、UI 微更新),對源控件和目標控件分別處理。可得到 Affix 數據支持。默認為 null。即不處理。
      • 設計 IDragDropPayload 公共接口,配置協變,是本擴展方法保持精干而又多面的關鍵。

      2. 消息傳遞:DragDropMessage<TSource, TTarget>

      通過泛型消息明確拖放類型,實現跨層業務邏輯解耦。 這里也配置了協變,便于 WeakReferenceMessenger 引用。使用反射權衡后的妥協。

      public interface IDragDropMessage
      {
          public IDragDropPayload SourcePayload { get; }
          public IDragDropPayload TargetPayload { get; }
      }
      
      public sealed class DragDropMessage<TSource, TTarget> : IDragDropMessage
          where TSource : View
          where TTarget : View
      {
          public required DragDropPayload<TSource> SourcePayload { get; init; }
          public required DragDropPayload<TTarget> TargetPayload { get; init; }
      
          IDragDropPayload IDragDropMessage.SourcePayload => SourcePayload;
          IDragDropPayload IDragDropMessage.TargetPayload => TargetPayload;
      }
      

      關鍵點

      • 類型安全:通過 TSourceTTarget 約束拖放的源/目標類型(如 LabelBorder)。
      • 數據透傳:通過 DataPackagePropertySet 傳遞擴展屬性,避免消息類字段膨脹。
      • 解耦業務:消息僅負責數據傳遞,具體邏輯由訂閱者(如 MainPage)處理。

      3. AsDraggable<TSource> 擴展方法

      通過擴展方法為任意控件注入拖放能力,屏蔽手勢識別細節。

          public static void AsDraggable<TSource>(this TSource source, object? sourceAffix = null, Action? sourceCallback = null)
              where TSource : View
          {
              // 創建并存儲 payload
              var payload = new DragDropPayload<TSource>
              {
                  View = source,
                  Affix = sourceAffix,
                  Callback = sourceCallback
              };
      
              // 覆蓋現有 payload(如果存在)
              dragPayloads.AddOrUpdate(source, payload);
      
              // 查找或創建 DragGestureRecognizer
              var dragGesture = source.GestureRecognizers.OfType<DragGestureRecognizer>().FirstOrDefault();
              if (dragGesture == null)
              {
                  dragGesture = new DragGestureRecognizer { CanDrag = true };
                  source.GestureRecognizers.Add(dragGesture);
      
                  // 只在首次添加手勢時注冊事件
                  dragGesture.DragStarting += (sender, args) =>
                  {
                      // 通過 dragPayloads 提取最新的 payload
                      if (dragPayloads.TryGetValue(source, out var dragPayload) && dragPayload is DragDropPayload<TSource> payload)
                      {
                          args.Data.Properties.Add("SourcePayload", payload);
                          source.Opacity = 0.5;
                      }
                  };
              }
          }
      

      4. AsDroppable<TSource, TTarget> 擴展方法

      public static void AsDroppable<TTarget>(this TTarget target, object? targetAffix = null, Action? targetCallback = null)
          where TTarget : View
      {
          AsDroppable<View, TTarget>(target, targetAffix, targetCallback);
      }
      
      public static void AsDroppable<TSource, TTarget>(this TTarget target, object? targetAffix = null, Action? targetCallback = null)
          where TSource : View
          where TTarget : View
      {
          var dropGesture = target.GestureRecognizers.OfType<DropGestureRecognizer>().FirstOrDefault();
          if (dropGesture is null)
          {
              dropGesture = new DropGestureRecognizer() { AllowDrop = true };
              target.GestureRecognizers.Add(dropGesture);
      
              DragDropPayload<TTarget> defaultPayload = new()
              {
                  View = target,
                  Affix = null,
                  Callback = null
              };
      
              _ = dropPayloads
                  .GetOrCreateValue(dropGesture)
                  .GetOrAdd(typeof(View).Name, _ => defaultPayload);
      
              dropGesture.DragOver += (sender, args) =>
              {
                  bool isSupported = args.Data.Properties.TryGetValue("SourcePayload", out _);
                  target.BackgroundColor = isSupported ? Colors.LightGreen : Colors.Transparent;
              };
      
              dropGesture.DragLeave += (sender, args) =>
              {
                  target.BackgroundColor = Colors.Transparent;
              };
      
              dropGesture.Drop += (s, e) => OnDroppablesMessage<TTarget>(target, dropGesture, e);
          }
      
          DragDropPayload<TTarget> sourceSpecificDropPayload = new()
          {
              View = target,
              Affix = targetAffix,
              Callback = targetCallback
          };
      
          var payloadDict = dropPayloads.GetOrCreateValue(dropGesture);
          _ = payloadDict.AddOrUpdate(typeof(TSource).Name, (s) => sourceSpecificDropPayload, (s, old) => sourceSpecificDropPayload);
      }
      

      核心機制

      • 手勢識別器:使用 DragGestureRecognizerDropGestureRecognizer 捕獲拖放事件。 保持實例唯一。
      • 類型映射表:靜態存儲器 dragPayloads / dropPayloads 存儲可支持的拖、放對象及其附加的數據,保持最新。
      • 消息注冊:為每種類型組合注冊唯一的消息處理函數,確保消息精準投遞。
      • 方法重載:AsDroppable ,無特殊數據和動作附加的,可簡化處理,毋須逐一注冊類型配對。

      二、關鍵實現細節

      1. ConditionalWeakTable

      DragDropExtensions 中,我們使用兩個 ConditionalWeakTable 實現狀態管理,保證拖放事件發生時傳遞最新約定的數據。

      ConditionalWeakTable 最大的好處是避免內存泄漏。用 View 或 GestureRecognizer 實例作為鍵,當該實例不再被別處引用時,內存回收機制會自動清除對應的鍵值對,無需用戶專門釋放內存。

      private static readonly ConditionalWeakTable<View, IDragDropPayload> dragPayloads = [];
      private static readonly ConditionalWeakTable<GestureRecognizer, ConcurrentDictionary<string, IDragDropPayload>> dropPayloads = [];
      

      2. dropPayloads

      為每個 DropGestureRecognizer 關聯源類型映射和對應該源類型所預先配置目標類型 TargetPayload。

      DragDropPayload<TTarget> sourceSpecificDropPayload = new()
      {
          View = target,
          Affix = targetAffix,
          Callback = targetCallback
      };
      
      var payloadDict = dropPayloads.GetOrCreateValue(dropGesture);
      _ = payloadDict.AddOrUpdate(typeof(TSource).Name, (s) => sourceSpecificDropPayload, (s, old) => sourceSpecificDropPayload);
      

      還貼心地預備好默認配置:

      DragDropPayload<TTarget> defaultPayload = new()
      {
          View = target,
          Affix = null,
          Callback = null
      };
      
      _ = dropPayloads
          .GetOrCreateValue(dropGesture)
          .GetOrAdd(typeof(View).Name, _ => defaultPayload);
      

      3 . dragPayloads

      源類型 SourcePayload 配置表,在 DragGestureRecognizer 首次配置時注冊,重復 AsDraggable 方法時更新。

      // 創建并存儲 payload
      var payload = new DragDropPayload<TSource>
      {
          View = source,
          Affix = sourceAffix,
          Callback = sourceCallback
      };
      
      // 覆蓋現有 payload(如果存在)
      dragPayloads.AddOrUpdate(source, payload);
      

      4 . IDragDropMessage / WeakReferenceMessenger

      反射獲取分類拖放消息,但需要統一發送:

      // 構建泛型類型
      Type genericMessageType = typeof(DragDropMessage<,>);
      Type constructedMessageType = genericMessageType.MakeGenericType(sourceType, typeof(TTarget));
      
      // 創建實例
      object? message = Activator.CreateInstance(constructedMessageType);
      if (message is null)
      {
          return;
      }
      
      // 設置屬性
      PropertyInfo sourceProp = constructedMessageType.GetProperty("SourcePayload")!;
      PropertyInfo targetProp = constructedMessageType.GetProperty("TargetPayload")!;
      sourceProp.SetValue(message, sourcePayload);
      targetProp.SetValue(message, targetPayload);
      
      // 核心動作
      _ = WeakReferenceMessenger.Default.Send<IDragDropMessage>((IDragDropMessage)message);
      

      三、 反射的優化

      嘗試了很多辦法,還是采用反射技術,最為直接。

      我并不喜歡使用反射。消耗大不說,現在 Microsoft 大力推進 Native AOT( Ahead Of Time)編譯,將.NET 代碼提前編譯為本機代碼,對反射的使用有約束,如果代碼中反射模式導致 AOT 編譯器無法靜態分析,就會產生裁剪警告,甚至可能導致編譯失敗或運行時異常。

      因此,在 .NET MAUI 的 AOT 編譯環境下,對反射泛型類型的創建需要特殊處理。這里通過 預編譯委托緩存 + 靜態類型注冊 的組合方案,實現了AOT 的泛型消息工廠。高效是肯定的,目前看來,是兼容的。

      使用 ConcurrentDictionary<string, HashSet<Type>> 存儲注冊的源類型和目標類型,通過 "Source""Target" 兩個鍵區分不同角色的類型集合, HashSet<Type> 確保類型唯一性,避免重復注冊。

      private static readonly ConcurrentDictionary<string, HashSet<Type>> registeredTypes = new();
      

      自動配對機制:當新類型注冊時,自動與已注冊的對立類型(源→目標,目標→源)創建所有可能的配對組合(靜態),確保 AOT 環境下反射可用。

      private static void RegisterType(string role, Type type)
      {
        // 獲取或創建對應角色的類型集合
        var types = registeredTypes.GetOrAdd(role, _ => []);
      
        // 添加類型并判斷是否為新增(返回true表示新增)
        if (types.Add(type))
        {
            // 新注冊的類型,補全所有可能的配對組合
            if (role == "Source")
            {
                // 源類型:與所有已注冊的目標類型配對
                if (registeredTypes.TryGetValue("Target", out var targetTypes))
                {
                    foreach (var targetType in targetTypes)
                    {
                        RegisterMessageFactory(type, targetType);
                    }
                }
            }
            else if (role == "Target")
            {
                // 目標類型:與所有已注冊的源類型配對
                if (registeredTypes.TryGetValue("Source", out var sourceTypes))
                {
                    foreach (var sourceType in sourceTypes)
                    {
                        RegisterMessageFactory(sourceType, type);
                    }
                }
            }
        }
      }
      

      反射泛型工廠:每個類型組合僅反射一次,生成的委托被緩存

      private static readonly ConcurrentDictionary<(Type source, Type target), Func<IDragDropPayload, IDragDropPayload, IDragDropMessage>> messageFactories = new();
      
      private static void RegisterMessageFactory(Type sourceType, Type targetType)
      {
          var key = (sourceType, targetType);
          messageFactories.GetOrAdd(key, _ => {
              // 僅首次執行反射
              var messageType = typeof(DragDropMessage<,>).MakeGenericType(sourceType, targetType);
              return (sourcePayload, targetPayload) => {
                  var message = Activator.CreateInstance(messageType)!;
                  // 設置屬性...
                  return (IDragDropMessage)message;
              };
          });
      }
      

      反射優化策略:后續調用直接執行委托,避免重復反射

      // 通過預注冊的工廠創建消息實例
      var key = (sourceType, typeof(TTarget));
      if (messageFactories.TryGetValue(key, out var factory))
      {
          var message = factory(sourcePayload, targetPayload);
      
          // 核心動作
          _ = WeakReferenceMessenger.Default.Send<IDragDropMessage>(message);
      }
      

      AOT 兼容性保障

      預編譯委托緩存方案,支持任意類型組合,僅首次注冊時有反射開銷,平衡靈活性和性能,但需要在編譯前靜態注冊所有可能的類型組合,避免運行時動態生成未知類型組合。

      必要的話,可使用 [assembly: Preserve] 屬性保留泛型類型及其成員。暫時沒采用這種方法,寄希望于 Microsoft 自行保證兼容性。

      四、使用示例

      MainPage.xaml

      <?xml version="1.0" encoding="utf-8" ?>
      <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                   xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                   x:Class="Zhally.DragDrop.MainPage"
                   Title="拖放示例">
      
          <StackLayout Spacing="20" Padding="30">
              <Label Text="高級拖放示例"
                     FontSize="22"
                     FontAttributes="Bold"
                     HorizontalOptions="Center" />
      
              <HorizontalStackLayout 
                      HorizontalOptions="Center">
                  <Label x:Name="DragLabel"
                      Text="拖放示例文本"
                      BackgroundColor="LightBlue"
                      Padding="12"
                      HorizontalOptions="Center"
                      FontSize="16" />
      
                  <BoxView x:Name="DragBoxView"
                      HeightRequest="60"
                      WidthRequest="120"
                      BackgroundColor="LightPink"
                      HorizontalOptions="Center" />
      
                  <ContentView x:Name="DragContentView"
                      HeightRequest="60"
                      WidthRequest="120"
                      BackgroundColor="LightCyan"
                      HorizontalOptions="Center" />
              </HorizontalStackLayout>
      
              <Border x:Name="DropBorder"
                     BackgroundColor="LightGreen"
                     Padding="20"
                     Margin="10"
                     HorizontalOptions="Center"
                     WidthRequest="200"
                     HeightRequest="100">
                  <Label Text="放置目標區域" HorizontalOptions="Center" />
              </Border>
      
              <Label x:Name="ResultLabel"
                     Text="等待拖放操作..."
                     HorizontalOptions="Center"
                     FontAttributes="Italic"
                     TextColor="Gray" />
          </StackLayout>
      </ContentPage>    
      

      MainPage.xaml.cs

      using CommunityToolkit.Mvvm.Messaging;
      using System.Diagnostics;
      using Zhally.DragDrop.Controls;
      
      namespace Zhally.DragDrop;
      
      public partial class MainPage : ContentPage
      {
          public MainPage()
          {
              InitializeComponent();
              SetupDragDrop();
          }
      
          private void SetupDragDrop()
          {
              // 設置可拖動元素(攜帶 Payload 數據)
              DragLabel.AsDraggable<Label>(
                  sourceAffix: new { Type = "文本數據", Value = "Hello World" },
                  sourceCallback: () => Debug.WriteLine("拖動源回調")
              );
              DragLabel.AsDraggable<Label>(
                  sourceAffix: new { Type = "文本數據", Value = "Hello World agian" },
                  sourceCallback: () => Debug.WriteLine("拖動源回調 again")
              );
              DragBoxView.AsDraggable<BoxView>(
                  sourceAffix: new { Type = "BoxView數據", Value = "BoxView" },
                  sourceCallback: () => Debug.WriteLine("按鈕拖動回調")
              );
              DragContentView.AsDraggable<ContentView>(
                  sourceAffix: new { Type = "ContentView數據", Value = "ContentView" },
                  sourceCallback: () => Debug.WriteLine("按鈕拖動回調")
              );
      
              // 設置可放置元素(攜帶目標數據)
              DropBorder.AsDroppable<Label, Border>(
                targetAffix: new { Type = "目標數據", Value = "Label Drop Zone" },
                targetCallback: () => Debug.WriteLine("放置目標回調")
              );
      
              DropBorder.AsDroppable<BoxView, Border>(
                targetAffix: new { Type = "目標數據", Value = "BoxView Drop Zone" },
                targetCallback: () => Debug.WriteLine("放置目標回調")
              );
      
              // 設置可放置元素(通用,非必須,在攜帶目標數據時有用)
              DropBorder.AsDroppable<Border>(
                targetAffix: new { Type = "目標數據", Value = "Generic Drop Zone" },
                targetCallback: () => Debug.WriteLine("放置目標回調")
              );
          }
      
          protected override void OnAppearing()
          {
              base.OnAppearing();
      
              WeakReferenceMessenger.Default.Register<IDragDropMessage>(this, HandleBorderDragDropMessage);
          }
      
          protected override void OnDisappearing()
          {
              base.OnDisappearing();
              WeakReferenceMessenger.Default.UnregisterAll(this);
          }
      
          private void HandleBorderDragDropMessage(object recipient, IDragDropMessage message)
          {
              if (message.SourcePayload.View == null || message.TargetPayload.View == null)
              {
                  return;
              }
      
              switch (message.SourcePayload.View)
              {
                  case Label label:
                      HandleLabelDrop(label, message);
                      break;
      
                  case BoxView boxView:
                      HandleBoxViewDrop(boxView, message);
                      break;
      
                  case ContentView contentView:
                      HandleContentViewDrop(contentView, message);
                      break;
      
                  default:
                      HandleDefaultDrop(message);
                      break;
              }
          }
      
          private void HandleDefaultDrop(IDragDropMessage message) => HandleBorderMessage(message);
          private void HandleLabelDrop(Label label, IDragDropMessage message) => HandleBorderMessage(message);
          private void HandleBoxViewDrop(BoxView boxView, IDragDropMessage message) => HandleBorderMessage(message);
          private void HandleContentViewDrop(ContentView contentView, IDragDropMessage message) => HandleBorderMessage(message);
          private void HandleBorderMessage(IDragDropMessage message)
          {
      
              MainThread.BeginInvokeOnMainThread(() =>
              {
                  ResultLabel.Text = $"拖放成功!\n" +
                                    $"源類型: {message.SourcePayload.View.GetType()}\n" +
                                    $"源數據: {message.SourcePayload.Affix}\n" +
                                    $"目標數據: {message.TargetPayload.Affix}";
              });
      
              // 執行回調
              MainThread.BeginInvokeOnMainThread(() =>
              {
                  message.SourcePayload.Callback?.Invoke();  // 執行源回調
              });
      
              // 執行回調
              MainThread.BeginInvokeOnMainThread(() =>
              {
                  message.TargetPayload.Callback?.Invoke();   // 執行目標回調
              });
          }
      
      }
      

      五、總結

      本方案實現了 MAUI 控件拖放能力的動態擴展。核心設計遵循以下原則:

      1. 解耦:拖放邏輯與控件分離,通過消息系統連接業務層。
      2. 類型安全:泛型約束確保拖放類型匹配,編譯期暴露潛在問題。
      3. 可擴展:通過字典映射和消息訂閱,輕松支持新的拖放類型組合。

      此方案已在實際項目中驗證,適用于文件管理、列表排序、數據可視化等場景,為 MAUI 應用提供了靈活高效的拖放解決方案。

      本方案源代碼開源,按照 MIT 協議許可。地址:xiaql/Zhally.Toolkit: Practices on specific tool kit in MAUI

      posted @ 2025-06-10 14:12  zhally  閱讀(297)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 亚洲一区在线成人av| 日韩人妻一区中文字幕| 久久天天躁狠狠躁夜夜躁| 亚洲色大成网站WWW久久| 国产成人综合亚洲第一区| 亚洲精品一区二区动漫| 精品一区二区三区四区激情| 日韩丝袜亚洲国产欧美一区| 国产毛片基地| 久久精品亚洲精品国产色婷| 国产v综合v亚洲欧美久久| 九九热视频在线精品18| 日本精品一区二区不卡| 92精品国产自产在线观看481页| 国产免费午夜福利在线播放| 国产精品男女爽免费视频| 土默特左旗| 国内少妇偷人精品免费| 中文字幕日韩一区二区不卡| 精品国产午夜福利在线观看| 色诱视频在线观看| 日韩乱码人妻无码中文字幕视频| 国产精品国产三级在线专区| 国产av人人夜夜澡人人爽麻豆| 亚洲AV无码不卡在线播放| 夜鲁鲁鲁夜夜综合视频| 国产成人综合亚洲精品国产| 日韩中文字幕av有码| 国产午夜亚洲精品不卡网站| 国内精品人妻无码久久久影院导航 | 99久久国产一区二区三区| 精品亚洲女同一区二区| 69人妻精品中文字幕| 国产偷倩视频| 美女黄网站18禁免费看| 亚洲精品成人片在线观看精品字幕| 亚洲av色香蕉一二三区| 男人的天堂av社区在线| 巨胸爆乳美女露双奶头挤奶| 亚洲高清免费在线观看| 十九岁的日本电影免费观看|