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

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

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

      Maui 實踐:再論為控件動態(tài)擴展 DragDrop 能力

      —— 不要把 DataPackagePropertySetView 看作一層皮

      夏群林 原創(chuàng) 2025.7.18

      一、Drag / Drop 之間傳遞的參數(shù)

      前文提到,拖放的實現(xiàn)需要 DragGestureRecognizer 與 DropGestureRecognizer 在不同的控件上相互配合,數(shù)據(jù)傳輸和配置復(fù)雜。主要有三個事件參數(shù):DragStartingEventArgs,DragEventArgs 和 DropEventArgs。還有一個 DropCompletedEventArgs,不涉及實體數(shù)據(jù)傳遞,這里不討論。

      Drag / Drop 操作,本質(zhì)上是把依存于源控件的數(shù)據(jù),與依存于目標(biāo)控件的數(shù)據(jù),挑選出來,組合,供業(yè)務(wù)流程調(diào)用。

      DragStartingEventArgs 是數(shù)據(jù)的起點,它打包了 DataPackage 類型的 Data,以及源控件的位置數(shù)據(jù)。位置數(shù)據(jù)暫且不論,我們聚焦在業(yè)務(wù)層面的實體數(shù)據(jù)上。去掉枝枝葉葉,在 Maui 中 DragStartingEventArgs 源代碼是這樣:

      public class DragStartingEventArgs : EventArgs
      {
          public bool Handled { get; set; }
      	public bool Cancel { get; set; }
      
      	public DataPackage Data { get; } = new DataPackage();
      
      	public virtual Point? GetPosition(Element? relativeTo) =>_getPosition?.Invoke(relativeTo);
      }
      

      展開 DataPackage,不神秘,就是一個在應(yīng)用程序中封裝和傳遞數(shù)據(jù)的容器,它的只讀屬性 Properties,一個鍵值對詞典,Dictionary<string, object> _propertyBag,是載體,用 DataPackagePropertySet 類包裝。object 類型的值,你可以在這里放任何你想傳遞的數(shù)據(jù)。所以,簡單,但強大。

      public class DataPackage
      {
          public DataPackagePropertySet Properties { get; }
      
          public ImageSource Image { get; set; }
          public string Text { get; set; }
          
          public DataPackageView View => new DataPackageView(this.Clone());
      }
      
      public class DataPackagePropertySet : IEnumerable
      {
          // 這里是數(shù)據(jù)保持處
      	Dictionary<string, object> _propertyBag;
          
      	public IEnumerable<string> Keys => _propertyBag.Keys;
      	public IEnumerable<object> Values => _propertyBag.Values;
      	public void Add(string key, object value)=> _propertyBag.Add(key, value);
      	public bool ContainsKey(string key) => _propertyBag.ContainsKey(key);
      	public bool TryGetValue(string key, out object value) => 
              _propertyBag.TryGetValue(key, out value);
      }
      

      DragEventArgs 是數(shù)據(jù)中間站,當(dāng)拖動源控件經(jīng)停目標(biāo)控件時,平臺會比對兩個控件,是否有緣。屬于 Drag / Drop 對相同陣營的,

      public class DragEventArgs : EventArgs
      {
      	public DragEventArgs(DataPackage dataPackage)
      	{
      		Data = dataPackage;
      	}
      	public DataPackage Data { get; }
      	public DataPackageOperation AcceptedOperation { get; set; } = DataPackageOperation.Copy;
      }
      

      就會允許 DataPackage 接收下來打包轉(zhuǎn)發(fā)。 AcceptedOperation 決定要不要 Copy。事實上,枚舉類型 DataPackageOperation 只有兩個值:Copy / None 。同樣,我們這里忽略了位置數(shù)據(jù)的討論。

      最后,DropEventArgs 將 DragStartingEventArgs 傳來的 DataPackage 蒙上面紗,以 DataPackageView 面目示人。

      public class DropEventArgs
      {
      	public DataPackageView Data { get; }
      	public bool Handled { get; set; }
      
      	public virtual Point? GetPosition(Element? relativeTo) =>
      		_getPosition?.Invoke(relativeTo);
      }
      
      public class DataPackageView
      {
          public DataPackagePropertySetView Properties { get; }
          
          public Task<ImageSource> GetImageAsync()
          {
              return Task.FromResult(DataPackage.Image);
          }
          
          public Task<string> GetTextAsync()
          {
              return Task.FromResult(DataPackage.Text);
          }
      }
      

      DataPackageView 的數(shù)據(jù)載體是 DataPackagePropertySetView,后者是 DataPackagePropertySet 的只讀包裝:

      public class DataPackagePropertySetView : IReadOnlyDictionary<string, object>
      {
      	public DataPackagePropertySet _dataPackagePropertySet;
      
      	public object this[string key] => _dataPackagePropertySet[key];
      	public IEnumerable<string> Keys => _dataPackagePropertySet.Keys;
      	public IEnumerable<object> Values => _dataPackagePropertySet.Values;
      	public int Count => _dataPackagePropertySet.Count;
      	public bool ContainsKey(string key) => _dataPackagePropertySet.ContainsKey(key);
      	public bool TryGetValue(string key, out object value) => _dataPackagePropertySet.TryGetValue(key, out value);
      }
      

      觀察 DataPackage / DataPackageView,會發(fā)現(xiàn),除了核心的用戶數(shù)據(jù)字典外,還有一個字符串?dāng)?shù)據(jù),string Text,一個圖像數(shù)據(jù),ImageSource Image。拖放操作,在 DragStartingEventArgs 時準(zhǔn)備數(shù)據(jù)。最后在 DropEventArgs 處獲取數(shù)據(jù):

      public Task<ImageSource> GetImageAsync()
      {
          return Task.FromResult(DataPackage.Image);
      }
      
      public Task<string> GetTextAsync()
      {
          return Task.FromResult(DataPackage.Text);
      }
      

      你可以把 Text / Image 看作常用數(shù)據(jù)快捷通道。我的實踐,就是利用這個快捷通道。

      二、DataPackagePropertySetView 的核心價值:不止于包裝

      我相信,初學(xué)者大多會有我當(dāng)初那樣的困惑:用 DataPackagePropertySetView 包裝 DataPackagePropertySet,是否多此一舉?既然底層實質(zhì)數(shù)據(jù)一樣,用同一個數(shù)據(jù)類型豈不方便?何必要加 DataPackagePropertySetView 這層皮?

      原因是,Maui 為我們帶來跨平臺數(shù)據(jù)標(biāo)準(zhǔn)化便利的同時,也帶來了跨平臺數(shù)據(jù)傳遞打包解包的繁雜,以及額外開銷。

      由于 Maui 控件的實現(xiàn),最終會轉(zhuǎn)化成應(yīng)用所在平臺的本機實現(xiàn),Drag / Drop 操作所攜帶的數(shù)據(jù),會一層層轉(zhuǎn)換為本機要求的結(jié)構(gòu),再一層層轉(zhuǎn)換回 Maui。這里的事情,不簡單。

      在 MAUI 拖放機制中,DataPackagePropertySetView 絕非簡單的字典包裝,而是跨平臺數(shù)據(jù)傳輸?shù)暮诵臉屑~,其設(shè)計蘊含三大關(guān)鍵價值:

      1. 跨平臺數(shù)據(jù)標(biāo)準(zhǔn)化。MAUI需要將數(shù)據(jù)轉(zhuǎn)換為不同平臺的原生格式(如Android的ClipData、iOS的NSItemProvider),而DataPackagePropertySetView通過標(biāo)準(zhǔn)化屬性(如Title、Description、Keywords)屏蔽了底層差異。

      2. 類型安全與延遲加載。強類型訪問,避免通過字符串鍵強制轉(zhuǎn)換類型的風(fēng)險(如(string)properties["Title"]);僅在調(diào)用GetTextAsync()等方法時才實際傳輸數(shù)據(jù),延遲加載,減少無效開銷。

      3. 安全隔離機制。作為只讀視圖,DataPackagePropertySetView防止拖放目標(biāo)意外修改源數(shù)據(jù),同時通過平臺適配器確保數(shù)據(jù)傳輸?shù)陌踩裕ㄈ缈邕M(jìn)程場景的序列化/反序列化)。

      不需要更細(xì)節(jié)的理解,但是我做了決定,能繞開依賴本機數(shù)據(jù)轉(zhuǎn)換而傳遞數(shù)據(jù)的,最好在 Maui 層面直接處理。這也是當(dāng)初我開發(fā) AsDroppable/ AsDraggable 擴展方法驅(qū)動力之一,但還不夠徹底。 (參閱: Maui 實踐:為控件動態(tài)擴展 DragDrop 能力 )。

      自定義數(shù)據(jù)傳遞,通常我們會建立全局緩存管理器,在拖放源緩存對象并傳遞 ID,然后在拖放目標(biāo)通過 ID 獲取對象。因為數(shù)據(jù)產(chǎn)生于拖放源,如果拖放源不再被引用,其所產(chǎn)生的數(shù)據(jù)也應(yīng)該銷毀,否則會造成內(nèi)存泄漏。

      問題在于,我們使用 AsDraggable 方法為拖放源配置拖放數(shù)據(jù)時,不知道該拖放源在程序邏輯中,是否要釋放,何時會釋放。于是想到用弱引用管理器避免內(nèi)存泄漏。

      ConditionalWeakTable<TKey, TValue> 是 .NET 框架中的一個特殊集合類,在兩個對象之間建立弱關(guān)聯(lián)關(guān)系,同時確保不會阻止垃圾回收(GC)對這些對象的回收。當(dāng) TKey 類型的對象(鍵)被垃圾回收時,對應(yīng)的 TValue 類型的值也會被自動從表中移除,不會因為鍵值對的存在而延長對象的生命周期。這樣,我們可以緩存與特定拖 / 放源關(guān)聯(lián)的數(shù)據(jù),同時不影響這些對象的垃圾回收。完美。

      不過,針對我的應(yīng)用情形,完美之中有瑕疵。我們的全局緩存管理器在拖放源緩存對象并傳遞 ID,這個 ID,我直接選用 Guid 類型,可以唯一性區(qū)別無限個數(shù)據(jù),簡潔。但 Guid 是值類型,不符合 ConditionalWeakTable<TKey, TValue> 對 TKey 為引用類型的要求。

      我設(shè)計了一個包裝類,把 Guid 包裝成引用類:

      public sealed class GuidToken
      {
          public Guid Id { get; } = Guid.NewGuid();
          public string Token => Id.ToString();
      }
      

      然后用 GuidToken 作為鍵,欺騙 ConditionalWeakTable:

      private static readonly ConditionalWeakTable<GestureRecognizer, GuidToken> guidTokens = [];
      private static readonly ConditionalWeakTable<GuidToken, DragDropPayload> dragDropPayloads = [];
      

      這里,拖 / 放源為鍵關(guān)聯(lián) GuidToken 值,再以 GuidToken 為鍵關(guān)聯(lián)數(shù)據(jù) DragDropPayload。這樣,我們就實現(xiàn)了在拖放過程中,當(dāng)平臺與本機做著復(fù)雜的交互時,只需傳遞簡單的 guid 字符串,還保證不會內(nèi)存泄漏。

      我們還要做點額外的工作:為 GuidToken 建立一個生命期可控的強引用:

      private static readonly ConcurrentDictionary<string, WeakReference<GuidToken>> tokenCache = new();
      

      否則,GuidToken 不知何時會被 GC,其代表的數(shù)據(jù)亦不知何時被 GC。手動控制 GuidToken 生命期的方式,結(jié)合在 AsDraggable / AsDroppable 擴展方法中,后面一并講到。

      三、進(jìn)階:DynamicGesturesExtension 改進(jìn)

      根據(jù)前面的討論,我對自己先前開發(fā)的 AsDraggable / AsDroppable 擴展方法予以改進(jìn)。AsDraggable / AsDroppable 是通用方法,本想通過泛型的方式,源控件類型/目標(biāo)控件類型的組合,來區(qū)分應(yīng)該采取的拖放后續(xù)操作。為此,還回避不了頭疼的反射技術(shù)。這次我順手把它去掉了,區(qū)分拖放后續(xù)操作,只需要通過拖/放控件關(guān)聯(lián)的數(shù)據(jù)類型 DragDropPayload 的組合,即可確認(rèn)。我也簡化了 DragDropPayload 數(shù)據(jù)結(jié)構(gòu),消除協(xié)變和逆變的顧忌。

      public class DragDropPayload
      {
          public required View View { get; init; }                        // 拖放源/目標(biāo)控件
          public object? Affix { get; init; }                             // 任意附加數(shù)據(jù)(如文本、對象)
          public Action<View, object?>? Callback { get; init; }           // 拖放完成后的回調(diào)
          public View? Anchor { get; set; } = null;                       // 拖放源/目標(biāo)控件的 recognizer 依附 View 組件
      
          public SourceTypeEnum SourceType { get; set; }                  // 標(biāo)識。源/目標(biāo)之間標(biāo)識有交集者才能交互
      }
      

      1. 注冊數(shù)據(jù)

      private static string RegisterPayload(this GestureRecognizer recognizer, DragDropPayload payload)
      {
          ArgumentNullException.ThrowIfNull(recognizer);
          ArgumentNullException.ThrowIfNull(payload);
      
          var guidToken = guidTokens.GetOrCreateValue(recognizer);
      
          dragDropPayloads.AddOrUpdate(guidToken, payload);
      
          tokenCache[guidToken.Token] = new WeakReference<GuidToken>(guidToken);
      
          return guidToken.Token;
      }
      

      注冊數(shù)據(jù)在指定源控件 AsDragble 時完成:

      public static void AsDraggable<TSourceAnchor, TSource>(this TSourceAnchor anchor, TSource source, 	       Func<TSourceAnchor, TSource, DragDropPayload> payloadCreator)
          where TSourceAnchor : View
          where TSource : View
      {
          AttachDragGestureRecognizer(anchor, source, payloadCreator); // 覆蓋現(xiàn)有 payload(如果存在)
      }
      
      private static void AttachDragGestureRecognizer<TSourceAnchor, TSource>(TSourceAnchor anchor, TSource source, Func<TSourceAnchor, TSource, DragDropPayload> payloadCreator)
          where TSourceAnchor : View
          where TSource : View
      {
          anchor.Undraggable();
          DragGestureRecognizer dragGesture = new() { CanDrag = true };
          anchor.GestureRecognizers.Add(dragGesture);
      
          dragGesture.DragStarting += (sender, args) =>
          {
              DragDropPayload dragPayload = payloadCreator(anchor, source);
              _ = dragGesture.RegisterPayload(dragPayload);
      
              args.Data.Text = guidTokens.GetOrCreateValue(dragGesture).Token;
              anchor.Opacity = 0.5;
          };
      
          dragGesture.DropCompleted += (sender, args) =>
          {
              guidTokens.GetOrCreateValue(dragGesture).Token.RemovePayload();
          };
      }
      

      2. 匹配數(shù)據(jù),在 DragLeave 事件中處理

      dropGesture.DragOver += (sender, e) =>
      {
          string token = e.Data.Text;
      
          if (token.TryAssociatedPayload(out DragDropPayload? dragPayload) &&
              guidTokens.TryGetValue(dropGesture, out GuidToken? dropToken) && dropToken is not null &&
              dropToken.Token.TryAssociatedPayload(out DragDropPayload? dropPayload) &&
              (dragPayload.SourceType & dropPayload.SourceType) != 0)
          {
              e.AcceptedOperation = DataPackageOperation.Copy;
          }
          else
          {
              e.AcceptedOperation = DataPackageOperation.None;
          }
      };
      
      public static bool TryAssociatedPayload(this string token, [NotNullWhen(true)] out DragDropPayload? payload)
      {
          payload = null;
          if (!token.IsValidGuid())
          {
              return false;
          }
      
          if (tokenCache.TryGetValue(token, out var weakGuidToken) &&
              weakGuidToken.TryGetTarget(out var guidToken) &&
              dragDropPayloads.TryGetValue(guidToken, out payload))
          {
              return true;
          }
      
          _ = tokenCache.TryRemove(token, out _);        // 嘗試清理緩存
          return false;
      }
      

      注意上面嘗試清理緩存,順手做的。在DropCompleted事件處理中,此時數(shù)據(jù)傳遞使命完成,會專門移除緩存數(shù)據(jù):

      dragGesture.DropCompleted += (sender, args) =>
      {
          guidTokens.GetOrCreateValue(dragGesture).Token.RemovePayload();
      };
      
      public static void RemovePayload(this string token)
      {
          if (!token.IsValidGuid() || !tokenCache.TryGetValue(token, out var weakToken))
          {
              return;
          }
      
          if (weakToken.TryGetTarget(out var guidToken))
          {
              _ = dragDropPayloads.Remove(guidToken);
          }
      
          _ = tokenCache.TryRemove(token, out _);
      }
      

      3. 發(fā)送數(shù)據(jù)組合,OnDroppablesMessageAsync

      public static void AsDroppable<TTargetAnchor, TTarget>(this TTargetAnchor anchor, DragDropPayload payload)
          where TTargetAnchor : View
          where TTarget : View
      {
          anchor.Undroppable();
          DropGestureRecognizer dropGesture = new() { AllowDrop = true };
          anchor.GestureRecognizers.Add(dropGesture);
          _ = dropGesture.RegisterPayload(payload);
      
          // ... ...
      
          dropGesture.Drop += async (s, e) =>
          {
              await OnDroppablesMessageAsync<TTargetAnchor>(anchor, dropGesture, e);
             // ... ...
          };
      }
      
      private static async Task OnDroppablesMessageAsync<TTargetAnchor>(TTargetAnchor anchor, DropGestureRecognizer dropGesture, DropEventArgs e)
       where TTargetAnchor : View
      {
          string token = await e.Data.GetTextAsync();
      	// ... ...
          _ = WeakReferenceMessenger.Default.Send<DragDropMessage>(new DragDropMessage()
          {
              SourcePayload = sourcePayload,
              TargetPayload = targetPayload
          });	
           // ... ...
      }
      

      MAUI拖放功能的核心挑戰(zhàn)不僅在于數(shù)據(jù)傳遞,更在于對象生命周期的安全管理。DataPackagePropertySetView通過標(biāo)準(zhǔn)化接口屏蔽了平臺差異,要謹(jǐn)慎對待其背后的復(fù)雜,而借助ConditionalWeakTable的個性化設(shè)計,則解決了復(fù)雜對象傳輸?shù)男阅芘c內(nèi)存問題。

      本 DynamicGesturesExtension 改進(jìn)方案已在實際項目中驗證,源代碼開源,按照 MIT 協(xié)議許可。地址:地址 xiaql/Zhally.Toolkit: Practices on specific tool kit in MAUI

      posted @ 2025-07-18 12:46  zhally  閱讀(262)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 国产高在线精品亚洲三区| 国产影片AV级毛片特别刺激| 2022最新国产在线不卡a| 免费无码又爽又刺激高潮虎虎视频| 亚洲欧美色综合影院| 亚洲一区二区乱码精品| 国产精品日韩中文字幕熟女| 性色在线视频精品| 少妇人妻偷人精品无码视频| 亚洲国产成人久久综合同性| 日区中文字幕一区二区| av日韩精品在线播放| 成年女人免费碰碰视频| 高清无打码一区二区三区| 在线看国产精品自拍内射| 自拍偷拍一区二区三区四| 国产一区精品综亚洲av| 一本无码在线观看| 亚洲精品一区二区三天美| 久久精品国产亚洲欧美| 野花韩国高清电影| 92自拍视频爽啪在线观看| 91中文字幕一区在线| 国产精品久久久一区二区三区| av在线播放日韩亚洲欧| 精品视频不卡免费观看| 亚洲一区二区三区 无码| 日韩精品毛片一区到三区| 亚洲av成人一区二区三区| 丁香花在线影院观看在线播放| 国产真人无遮挡免费视频| 狠狠躁夜夜躁人人爽天天5| 22222se男人的天堂| 成人午夜av在线播放| 一区二区三区精品不卡| 西西午夜无码大胆啪啪国模| 国产欧美日韩视频一区二区三区| 大地资源中文第二页日本| 亚洲日韩av无码一区二区三区人 | 69精品丰满人妻无码视频a片| 亚洲成在人天堂一区二区|