Freezable objects do not require attachment to the WPF visual tree, maintain a persistent lifetime, and serve as a reliable binding relay between a detached ContextMenu and its parent host control.

A Freezable derived object does not require attachment to the WPF visual tree, holds a persistent lifetime once instantiated as a logical resource, and serves as a stable binding relay to connect a detached ContextMenu back to its original placement host control and its DataContext.

 

  • Exist without visual tree: The Freezable proxy lives only in the logical resource dictionary, never added to any visual tree node, yet fully participates in WPF binding system.
  • Lives persisted: Instance lifetime is tied to the host control’s resource container, unaffected by repeated creation/destruction of the floating ContextMenu Popup.
  • Acts as relay/bridge: Creates a fixed binding tunnel across two isolated visual trees, letting menu items reliably access the original host’s ViewModel without code-behind, adhering strictly to MVVM.

 

public class ProxyBinding : Freezable
{
    protected override Freezable CreateInstanceCore()
    {
        return new ProxyBinding();
    }


    public object SourceObject
    {
        get { return (object)GetValue(SourceObjectProperty); }
        set { SetValue(SourceObjectProperty, value); }
    }

    // Using a DependencyProperty as the backing store for SourceObject.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SourceObjectProperty =
        DependencyProperty.Register(
            nameof(SourceObject), 
            typeof(object), 
            typeof(ProxyBinding), 
            new PropertyMetadata(null));


}

 <UserControl.Resources>
     <local:ProxyBinding x:Key="ProxyBinding"
                         SourceObject="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}"/>
 </UserControl.Resources>

 <DataGrid.ContextMenu>
     <ContextMenu>
         <MenuItem Header="{Binding SourceObject.UCFirstHeader,
                   Source={StaticResource ProxyBinding}}"
                   Command="{Binding SourceObject.UCFirstCmd,
                   Source={StaticResource ProxyBinding}}"
                   CommandParameter="{Binding Path=PlacementTarget.SelectedItems,
                   RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"
                   Width="300"
                   FontSize="30"/>
     </ContextMenu>
 </DataGrid.ContextMenu>

 

 

<UserControl x:Class="WpfApp4.UCDataGrid"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             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:WpfApp4"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <local:ProxyBinding x:Key="ProxyBinding"
                            SourceObject="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}"/>
    </UserControl.Resources>
    <Grid>
        <DataGrid ItemsSource="{Binding UCDGCollection,
                  RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type UserControl}}}"
                  VirtualizingPanel.IsVirtualizing="True"
                  VirtualizingPanel.VirtualizationMode="Recycling"
                  VirtualizingPanel.CacheLengthUnit="Item"
                  VirtualizingPanel.CacheLength="5,5"
                  ScrollViewer.CanContentScroll="True"
                  ScrollViewer.IsDeferredScrollingEnabled="True"
                  EnableRowVirtualization="True"
                  EnableColumnVirtualization="True"
                  CanUserAddRows="False"
                  AutoGenerateColumns="True"
                  SelectionMode="Extended">
            <DataGrid.Resources>
                <Style TargetType="DataGridRow">
                    <Setter Property="FontSize" Value="30"/>
                    <Style.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="FontSize" Value="50"/>
                            <Setter Property="Foreground" Value="Red"/>
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </DataGrid.Resources>
            <DataGrid.ContextMenu>
                <ContextMenu>
                    <MenuItem Header="{Binding SourceObject.UCFirstHeader,
                              Source={StaticResource ProxyBinding}}"
                              Command="{Binding SourceObject.UCFirstCmd,
                              Source={StaticResource ProxyBinding}}"
                              CommandParameter="{Binding Path=PlacementTarget.SelectedItems,
                              RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"
                              Width="300"
                              FontSize="30"/>
                </ContextMenu>
            </DataGrid.ContextMenu>
        </DataGrid>
    </Grid>
</UserControl>



using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp4
{
    /// <summary>
    /// Interaction logic for UCDataGrid.xaml
    /// </summary>
    public partial class UCDataGrid : UserControl
    {
        public UCDataGrid()
        {
            InitializeComponent();
        }



        public IList UCDGCollection
        {
            get { return (IList)GetValue(UCDGCollectionProperty); }
            set { SetValue(UCDGCollectionProperty, value); }
        }

        // Using a DependencyProperty as the backing store for UCDGCollection.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty UCDGCollectionProperty =
            DependencyProperty.Register(
                nameof(UCDGCollection), 
                typeof(IList), 
                typeof(UCDataGrid), 
                new PropertyMetadata(null));



        public string UCFirstHeader
        {
            get { return (string)GetValue(UCFirstHeaderProperty); }
            set { SetValue(UCFirstHeaderProperty, value); }
        }

        // Using a DependencyProperty as the backing store for UCFirstHeader.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty UCFirstHeaderProperty =
            DependencyProperty.Register(
                nameof(UCFirstHeader), 
                typeof(string), 
                typeof(UCDataGrid), 
                new PropertyMetadata(null, OnUCFirstHeaderChanged));

        private static void OnUCFirstHeaderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            
        }


        public DelCmd UCFirstCmd
        {
            get { return (DelCmd)GetValue(FirstCmdProperty); }
            set { SetValue(FirstCmdProperty, value); }
        }

        // Using a DependencyProperty as the backing store for FirstCmd.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty FirstCmdProperty =
            DependencyProperty.Register(
                nameof(UCFirstCmd), 
                typeof(DelCmd), 
                typeof(UCDataGrid), 
                new PropertyMetadata(null));



        public object FirstCmdParameter
        {
            get { return (object)GetValue(FirstCmdParameterProperty); }
            set { SetValue(FirstCmdParameterProperty, value); }
        }

        // Using a DependencyProperty as the backing store for FirstCmdParameter.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty FirstCmdParameterProperty =
            DependencyProperty.Register(
                nameof(FirstCmdParameter),
                typeof(object), 
                typeof(UCDataGrid),
                new PropertyMetadata(null));



    }


    public class ProxyBinding : Freezable
    {
        protected override Freezable CreateInstanceCore()
        {
            return new ProxyBinding();
        }


        public object SourceObject
        {
            get { return (object)GetValue(SourceObjectProperty); }
            set { SetValue(SourceObjectProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SourceObject.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SourceObjectProperty =
            DependencyProperty.Register(
                nameof(SourceObject), 
                typeof(object), 
                typeof(ProxyBinding), 
                new PropertyMetadata(null));


    }

    public class DelCmd : ICommand
    {
        private readonly Action<object?> execute;
        private readonly Func<object?, bool>? canExecute;
        public DelCmd(Action<object?> executeValue, Func<object?, bool>? canExecuteValue=null)
        {
            execute = executeValue ?? throw new ArgumentNullException(nameof(executeValue));
            canExecute = canExecuteValue;
        }

        public event EventHandler? CanExecuteChanged;

        public bool CanExecute(object? parameter)
        {
            return canExecute == null ? true : canExecute(parameter);
        }

        public void Execute(object? parameter)
        {
            if(!CanExecute(parameter))
            {
                return;
            }
            execute?.Invoke(parameter);
        }

        public void RaiseCanExecuteChanged()
        {
            var handler = Volatile.Read(ref CanExecuteChanged);
            if(handler==null)
            {
                return;
            }

            if (Application.Current?.Dispatcher?.CheckAccess()==true)
            {
                handler?.Invoke(this, EventArgs.Empty);
            }
            else
            {
                Application.Current?.Dispatcher?.Invoke(() =>
                {
                    handler?.Invoke(this, EventArgs.Empty);
                });
            }
        }
    }
}


<Window x:Class="WpfApp4.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp4"
        mc:Ignorable="d"
        Title="{Binding MainTitle}"
        WindowState="Maximized">
    <Window.DataContext>
        <local:MainVM/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <local:UCDataGrid UCDGCollection="{Binding BooksCollection}"
                          UCFirstHeader="{Binding DataContext.FirstHeader,
                          RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                          UCFirstCmd="{Binding FirstCmd}"                          
                          Grid.Row="0" 
                          Grid.Column="0" 
                          Grid.ColumnSpan="2"/>
        
        <DataGrid Grid.Row="1" 
                  Grid.Column="0" 
                  Grid.ColumnSpan="2"
           ItemsSource="{Binding BooksCollection}"
           VirtualizingPanel.IsVirtualizing="True"
           VirtualizingPanel.VirtualizationMode="Recycling"
           VirtualizingPanel.CacheLengthUnit="Item"
           VirtualizingPanel.CacheLength="5,5"
           ScrollViewer.CanContentScroll="True"
           ScrollViewer.IsDeferredScrollingEnabled="True"
           EnableRowVirtualization="True"
           EnableColumnVirtualization="True"
           CanUserAddRows="False"
           AutoGenerateColumns="True">
            <DataGrid.Resources>
                <Style TargetType="DataGridRow">
                    <Setter Property="FontSize" Value="30"/>
                    <Style.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="FontSize" Value="50"/>
                            <Setter Property="Foreground" Value="Red"/>
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </DataGrid.Resources>
        </DataGrid>
    </Grid>
</Window>


using Newtonsoft.Json;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace WpfApp4
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }

    public class MainVM : INotifyPropertyChanged
    {
        HttpClient client;
        string originalUrl = "http://localhost:8080/getbookslist?count=";
        private DispatcherTimer tmr;
        private bool isLoading = false;
        public MainVM()
        {
            if (!DesignerProperties.GetIsInDesignMode(new DependencyObject()))
            {
                client = new HttpClient();
                FirstHeader = "First MenuItem";
                Task.Run(async () =>
                {
                    await LoadDataFromServerRelentless();
                });
            }
        }

        private DelCmd firstCmd;
        public DelCmd FirstCmd
        {
            get
            {
                if(firstCmd==null)
                {
                    firstCmd = new DelCmd(FirstCmdExecuted);
                }
                return firstCmd;
            }
        }

        private void FirstCmdExecuted(object? obj)
        {
            var items = ((System.Collections.IList)obj).Cast<Book>()?.ToList();
            if (items!=null && items.Any())
            {
                MessageBox.Show($"Selected {items.Count} items", $"{DateTime.Now}");
            }
        }

        private async Task LoadDataFromServerRelentless(int cnt = 100000)
        {
            while (true)
            {
                try
                {
                    await InitBooksCollectionAsync(cnt);
                    await Task.Delay(10000);
                }
                catch (Exception ex)
                {
#if DEBUG
                    System.Diagnostics.Debug.WriteLine($"{DateTime.Now},{ex?.Message},{ex?.StackTrace?.ToString()}");
#else
                System.Diagnostics.Trace.WriteLine($"{DateTime.Now},{ex?.Message},{ex?.StackTrace?.ToString()}");
#endif
                }
            }
        }

        private async Task InitBooksCollectionAsync(int cnt = 1000000)
        {
            if (isLoading)
            {
                return;
            }
            isLoading = true;
            
            await Application.Current?.Dispatcher?.InvokeAsync(() =>
            {
                MainTitle = $"{DateTime.Now},loading...";
                BooksCollection?.Clear();
            }, DispatcherPriority.Background);

            try
            {
                string requestUrl = $"{originalUrl}{cnt}";
                string jsonStr = await client.GetStringAsync(requestUrl);
                if (string.IsNullOrWhiteSpace(jsonStr))
                {
                    return;
                }

                List<Book>? bksList = JsonConvert.DeserializeObject<List<Book>>(jsonStr);
                if (bksList != null && bksList.Any())
                {
                    await Application.Current?.Dispatcher?.InvokeAsync(() =>
                    {
                        BooksCollection = new ObservableCollection<Book>(bksList);
                        MainTitle = $"{DateTime.Now}," +
                        $"loaded {booksCollection.Count} items," +
                        $"First Id:{BooksCollection[0]?.Id}," +
                        $"Last Id:{BooksCollection[^1]?.Id}";
                    }, System.Windows.Threading.DispatcherPriority.Background);
                }
            }
            catch (Exception ex)
            {
#if DEBUG
                System.Diagnostics.Debug.WriteLine($"{DateTime.Now},{ex?.Message},{ex?.StackTrace?.ToString()}");
#else
                System.Diagnostics.Trace.WriteLine($"{DateTime.Now},{ex?.Message},{ex?.StackTrace?.ToString()}");
#endif
            }
            finally
            {
                isLoading = false;
            }
        }

        private string mainTitle = $"{DateTime.Now}";
        public string MainTitle
        {
            get
            {
                return mainTitle;
            }
            set
            {
                if (value != mainTitle)
                {
                    mainTitle = value;
                    OnPropertyChanged();
                }
            }
        }

        private string firstHeader;
        public string FirstHeader
        {
            get
            {
                return firstHeader;
            }
            set
            {
                if (value != firstHeader)
                {
                    firstHeader = value;
                    OnPropertyChanged();
                }
            }
        }

        private ObservableCollection<Book> booksCollection;
        public ObservableCollection<Book> BooksCollection
        {
            get
            {
                return booksCollection;
            }
            set
            {
                if (value != booksCollection)
                {
                    booksCollection = value;
                    OnPropertyChanged();
                }
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] string propName = "")
        {
            var handler = Volatile.Read(ref PropertyChanged);
            if (handler == null)
            {
                return;
            }
            handler?.Invoke(this, new PropertyChangedEventArgs(propName));
        }
    }

    public class Book
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string ISBN { get; set; }
        public string Author { get; set; }
        public string Comment { get; set; }
        public string Content { get; set; }
        public string Summary { get; set; }
        public string Title { get; set; }
        public string Topic { get; set; }
    }
}

 

 

 

 

image

 

 

 

image

 

posted @ 2026-06-13 19:50  FredGrit  阅读(8)  评论(0)    收藏  举报