简体   繁体   中英

WPF force xaml to re-fetch the property's value although the property's setter didn't change

I'm modifying an existing WPF project at work (I don't have much experience with WPF), and I have this property:

public Point WidgetMiddlePoint
    {
        get
        {
            return new PointByAppMonitorDPI(_middlePoint);
            //return _middlePoint;
        }
    }

And this at the UI side:

<controls1:BorderWithTip.TipOffset>
    <MultiBinding Converter="{StaticResource TipOffsetPositionConverter}">
        <Binding Path="WidgetMiddlePoint" Delay="500"  NotifyOnSourceUpdated="False" NotifyOnTargetUpdated="False"/>
        <Binding ElementName="BorderWithTip" Path="ActualWidth" Delay="500" NotifyOnSourceUpdated="False" NotifyOnTargetUpdated="False"/>
    </MultiBinding>
</controls1:BorderWithTip.TipOffset>

The TipOffsetPositionConverter performs some calculations based on the given parameters.
My problem is that the WidgetMiddlePoint value depends on the DPI of the monitor in which the app resides (the DPI isn't relevant for my question, it's just a use case for a factor that is taken into account only when calling the getter).
So what happens is that the UI takes the value from the getter and won't refresh that value unless I use the setter to set it to something else, and then 'notify'.

How can I configure the UI to re-get the value every time, even when it 'thinks' that the property's value hasn't changed? Or is it bad practice and not recommended?

If you want the the framework to call your getter, and consequently your converter, you should implement INotifyPropertyChanged and raise the PropertyChanged event in your view model.

You need to somehow determine when the DPI changes and then raise the event. The Convert method is only called whenever the framework is notified of a change of any of the data-bound properties ( WidgetMiddlePoint and ActualWidth in this case).

There's a Window DpiChanged event you can use for this, along with INotifyPropertyChanged.

The code below shows how to do this. It has a RecalculateMiddlePoint method that creates a test Point that has the current DPI for both X and Y values, but clearly it should do the appropriate calculation.

If you create a WPF app, the code below binds the middle point to a label and hence shows the changing DPI on the main window as you drag it between screens. The code works in both .NET Framework 4.8 and .NET Core 3.1.

C#

public partial class MainWindow : Window, INotifyPropertyChanged
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
        // Hook the DpiChanged event
        this.DpiChanged += Window_DpiChanged;
        // Initialize our bound property
        WidgetMiddlePoint = RecalculateMiddlePoint(VisualTreeHelper.GetDpi(this));
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void Window_DpiChanged(object sender, DpiChangedEventArgs e)
    {
        Debug.WriteLine($"Old Scale: {e.OldDpi.DpiScaleX} New Scale: {e.NewDpi.DpiScaleX} " + 
                        $"Old PPI: {e.OldDpi.PixelsPerInchX} New PPI: {e.NewDpi.PixelsPerInchX}");
        // Recalculate _widgetMiddlePoint based on the values above and just set it
        WidgetMiddlePoint = RecalculateMiddlePoint(e.NewDpi);
    }

    private Point RecalculateMiddlePoint(DpiScale newDpi)
    {
        // Recalculate based on the new DPI in here
        // For testing we just create a 'Point' that has the PPI for X and Y values
        return new Point(newDpi.PixelsPerInchX, newDpi.PixelsPerInchX);
        //return new PointByAppMonitorDPI(_middlePoint);  // Correct code????
    }

    private Point _middlePoint;
    public Point WidgetMiddlePoint
    {
        get { return _middlePoint; }
        set
        {
            _middlePoint = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WidgetMiddlePoint)));
        }
    }
}

XAML

<Window x:Class="WpfApp9.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:WpfApp9"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Label Content="{Binding Path=WidgetMiddlePoint}" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Top" Width="120"/>
    </Grid>
</Window>

Add to an app.manifest:

  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"> PerMonitor</dpiAwareness>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
    </windowsSettings>
  </application>

There is a PropertyChanged Add-in for Fody

https://github.com/Fody/PropertyChanged It takes away a bit of the boilerplate for using INotifyPropertyChanged

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM