Caliburn.Micro传元素ActualWidth/ActualHeight到ViewModel为0如何解决
这是Caliburn.Micro参数绑定的已知行为:cm:Message.Attach里内嵌的{Binding}表达式只会在XAML加载初始化阶段执行一次取值,不会像普通WPF Binding那样持续监听属性变更。初始化时控件布局还没完成,ActualWidth/ActualHeight的默认值就是0,后续点击事件触发时CM不会重新计算参数值,所以传过去的永远是0。
你在界面上放的Label能正常显示数值,是因为普通WPF Binding对这类布局相关属性有特殊适配,会在布局流程跑完后自动刷新目标值,CM的Action参数逻辑没做这层适配。
方案1:Tag中转传控件实例(改动最小)
直接把MainGrid实例绑定到按钮的Tag属性,用CM内置的$source特殊参数指代触发事件的按钮,点击时直接读取Tag中存储的MainGrid的实时尺寸,几乎不需要改动原有代码:
<Button Width="20" Height="20" Margin="5,5,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Tag="{Binding ElementName=MainGrid}" cm:Message.Attach="[Event Click] = [Action LaunchMirrorView($source.Tag.ActualWidth, $source.Tag.ActualHeight)]" ToolTip="Expand View"> <materialDesign:PackIcon Background="Transparent" Foreground="{StaticResource TealMidBrush}" Kind="ArrowExpandAll" /> </Button>
这个写法不需要修改ViewModel的方法签名,点击触发时取到的是布局完成后的实时尺寸,不会出现0值问题。
方案2:附加属性同步尺寸(严格符合MVVM)
如果不想把UI元素实例传到ViewModel层,可以写一个简单的布局监听附加属性,在尺寸变化时自动把ActualWidth/ActualHeight同步到ViewModel的对应属性,点击时直接读取ViewModel存储的最新值即可。
首先定义附加属性:
public static class LayoutTracking { public static readonly DependencyProperty TrackActualSizeProperty = DependencyProperty.RegisterAttached( "TrackActualSize", typeof(bool), typeof(LayoutTracking), new PropertyMetadata(false, OnTrackToggled)); public static void SetTrackActualSize(DependencyObject element, bool value) => element.SetValue(TrackActualSizeProperty, value); public static bool GetTrackActualSize(DependencyObject element) => (bool)element.GetValue(TrackActualSizeProperty); private static void OnTrackToggled(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not FrameworkElement fe) return; if ((bool)e.NewValue) { fe.SizeChanged += OnElementSizeChanged; if (fe.IsLoaded) { SyncSizeToVm(fe); } else { fe.Loaded += (_, _) => SyncSizeToVm(fe); } } else { fe.SizeChanged -= OnElementSizeChanged; } } private static void OnElementSizeChanged(object sender, SizeChangedEventArgs e) { if (sender is FrameworkElement fe) SyncSizeToVm(fe); } private static void SyncSizeToVm(FrameworkElement fe) { if (fe.DataContext is MainViewModel vm) { vm.CurrentGridWidth = fe.ActualWidth; vm.CurrentGridHeight = fe.ActualHeight; } } }
XAML中给MainGrid开启追踪即可:
<Grid x:Name="MainGrid" local:LayoutTracking.TrackActualSize="True"> <!-- 其余布局代码不变,按钮点击直接调用无参方法,直接用ViewModel中存储的最新尺寸即可 -->
这个方案完全不破坏MVVM分层,尺寸变化时会自动同步,不需要在事件触发时临时取值。
方案3:后置代码中转(快速实现)
如果项目对MVVM严格度要求不高,可以直接给MainGrid加SizeChanged事件,在后置代码中把尺寸同步给ViewModel,点击按钮时直接读取ViewModel存储的尺寸即可,不需要传参:
// MainGrid的SizeChanged事件处理 private void MainGrid_SizeChanged(object sender, SizeChangedEventArgs e) { if (DataContext is MainViewModel vm) { vm.CurrentGridWidth = e.NewSize.Width; vm.CurrentGridHeight = e.NewSize.Height; } }
内容的提问来源于stack exchange,提问作者GeoDirk

