【问题标题】:Binding fires on unloaded view/view-model when creating a new view创建新视图时在卸载的视图/视图模型上触发绑定
【发布时间】:2017-03-27 01:22:48
【问题描述】:

如果我使用类型转换器将 RadioButton 绑定到视图模型属性,则每次创建视图时,都会调用前一个 ViewModel 上的设置器,即使该视图已卸载且不应再存在。这是重现该问题的最少代码:

1) 定义一个枚举类型:

enum EnumType {
   Value1,
   Value2,
}

2) 定义一个转换器:

public class EnumTypeToBooleanConverter : IValueConverter {
    public object Convert(object value, Type targetType, object parameter, string language) {
      return true;
   }
   public object ConvertBack(object value, Type targetType, object parameter, string language) {
      return EnumType.Value1; 
   }
}

3) 定义视图模型:

class ViewModel : INotifyPropertyChanged {
    private EnumType value;
     public ViewModel() {
         Debug.WriteLine(string.Format("ViewModel ({0})::ctor", this.GetHashCode()));
    }
    public EnumType Value {
        get {
            Debug.WriteLine(string.Format("ViewModel ({0})::Value::get", this.GetHashCode()));
            return this.value;
        }
        set {
            Debug.WriteLine(string.Format("ViewModel ({0})::Value::set", this.GetHashCode()));
            if (this.value != value) {
                this.value = value;
                this.OnPropertyChanged();
            }
        }
    }
    private void OnPropertyChanged([CallerMemberName] string name = null) {
        if (this.PropertyChanged != null) {
            var ea = new PropertyChangedEventArgs(name);
            this.PropertyChanged(this, ea);
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

4) 定义一个用户控件 (View.xaml)

<UserControl
    x:Class="BindingIssue.View"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:BindingIssue"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400"
    x:Name="root">

   <UserControl.DataContext>
       <local:ViewModel x:Name="ViewModel"/>
   </UserControl.DataContext>

    <Grid>
       <ScrollViewer>       
           <StackPanel>
              <RadioButton x:Name="rdo1"
                           Content="Value1"
                           IsChecked="{Binding Path=Value, Converter={StaticResource EnumTypeToBooleanConverter}, ConverterParameter=Value1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
                <Button x:Name="btnClose"
                        Click="btnClose_Click"
                        Content="Close"/>
           </StackPanel>
       </ScrollViewer>
   </Grid>

5) 在 View 后面添加代码:

public View() {
     Debug.WriteLine(string.Format("View ({0})::ctor", this.GetHashCode()));
     this.InitializeComponent();
     this.Loaded += OnLoaded;
     this.Unloaded += OnUnloaded;
}
private void btnClose_Click(object sender, RoutedEventArgs e) {
    if (this.Parent is Popup) {
        Debug.WriteLine("Closing the popup...");
        ((Popup)this.Parent).IsOpen = false;
    }
}
private void OnLoaded(object sender, RoutedEventArgs e) {
    Debug.WriteLine(string.Format("View ({0})::Loaded", this.GetHashCode()));
  }
private void OnUnloaded(object sender, RoutedEventArgs e) {
    Debug.WriteLine(string.Format("View ({0})::Unloaded", this.GetHashCode()));
}

6) 主页 (XAML)

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
         x:Name="Grid">
    <Button x:Name="btnNewView"
              Click="btnNewView_Click"
              Content="New View"
              Margin="4"/>
</Grid>

7) 将事件处理程序添加到 MainPage

private void btnNewView_Click(object sender, RoutedEventArgs e) {
    Debug.WriteLine("Opening a new popup...");
    View view = new View();
    view.HorizontalAlignment = HorizontalAlignment.Center;
    view.VerticalAlignment = VerticalAlignment.Center;

    Popup popup = new Popup();
    popup.Child = view;
    popup.HorizontalOffset = 300;
    popup.VerticalOffset = 300;
    popup.IsOpen = true;
}

多次打开和关闭弹窗会产生以下输出(请注意哈希码):

正在打开一个新的弹出窗口...

查看 (46418718)::ctor

ViewModel (59312528)::ctor

ViewModel (59312528)::Value::get

查看 (46418718)::加载

关闭弹出窗口...

查看 (46418718)::Unloaded

正在打开一个新的弹出窗口...

查看 (58892413)::ctor

ViewModel (61646925)::ctor

ViewModel (61646925)::Value::get

ViewModel (59312528)::Value::set

查看 (58892413)::加载

关闭弹出窗口...

查看 (58892413)::卸载

这意味着在 Unloaded 视图模型中创建的 ViewModel 的 setter 被调用,这有点奇怪。 x:Bind 和 Binding 的这种行为是相同的。

我想知道是否有对此行为的解释。

澄清更多: 每次都会创建一对全新的视图/视图模型实例,但是当加载新视图时,会调用前一个视图模型实例的设置器。视图的前一个实例已卸载,此时甚至不应该存在。 (想想每次都关闭的弹出窗口,并且没有事件引用旧视图/视图模型。)

【问题讨论】:

    标签: xaml uwp win-universal-app uwp-xaml


    【解决方案1】:

    这意味着在 Unloaded 视图中创建的 ViewModel 的设置器 调用模型有点奇怪

    首先,setter 在视图卸载时不会被调用,它在视图加载时被调用。您可以添加Loading 事件句柄来验证这一点。在view控件后面的代码中添加加载事件代码如下:

      this.Loading += View_Loading;       
      private void View_Loading(FrameworkElement sender, object args)
      {
          Debug.WriteLine(string.Format("View ({0})::Loading", this.GetHashCode()));
      }
    

    现在的输出将是:

    Closing the popup...
    View (22452836)::Unloaded
    Opening a new popup...
    View (58892413)::ctor
    ViewModel (61646925)::ctor
    View (58892413)::Loading
    ViewModel (61646925)::Value::get
    ViewModel (19246503)::Value::set
    View (58892413)::Loaded
    

    其次,我们需要研究为什么在这种情况下会调用setter。 一是因为您将绑定模式设置为TwoWay。如果您按以下方式删除此属性,您将看不到调用 setter,因为 ViewModel 不需要知道 view 中的更改。

    <RadioButton x:Name="rdo1"  Content="Value1"  IsChecked="{Binding Path=Value, Converter={StaticResource EnumTypeToBooleanConverter}, ConverterParameter=Value1,  UpdateSourceTrigger=PropertyChanged}"/>
    

    更多绑定方式请参考this article。另一个原因可能是特定于RadioButton 的控件。 RadioButton 可以通过单击同组中的另一个 RadioButton 来清除,但不能通过再次单击来清除。所以当IsChecked属性设置为true时,我们认为组的属性值被更新了。这将触发TwoWay 绑定。在您的场景中,您可以通过将IsChecked 的默认值设置为false 来测试这一点,如下所示,您会发现直到您在UI 上选择rdo1 后才会调用setter。或者您可以使用另一个控件CheckBox 进行测试,在IsChecked 值更新之前也不会调用setter

    public class EnumTypeToBooleanConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            return false;
        }
        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
           return EnumType.Value1;       
        }
    }
    

    如果 ScrollViewer 从视图中移除,则行为不一样 让我们说布尔属性的行为是不一样的

    对于这两种情况,我这边也进行了测试。结果与上面的输出相同。由于我不知道你是如何绑定Boolean属性的,正如我提到的,是否调用setter取决于绑定模式是什么以及你是否设置或更新属性。我关于绑定Boolean 的测试代码如下,输出相同。

    View.xaml

    <RadioButton x:Name="rdo2"
               Content="BoolValue"
               IsChecked="{Binding Path=BoolValue, Converter={StaticResource EnumTypeToBooleanConverter}, ConverterParameter=Value1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    

    转换器:

    public class EnumTypeToBooleanConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            return true;
        }
        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            //return EnumType.Value1;     
            return true;  
        }
    }
    

    视图模型;

       private bool boolvalue;
       public bool BoolValue
       {
           get
           {
               Debug.WriteLine(string.Format("ViewModel ({0})::boolvalue::get", this.GetHashCode()));
               return this.boolvalue;
           }
           set
           {
               Debug.WriteLine(string.Format("ViewModel ({0})::boolvalue::set", this.GetHashCode()));
               if (this.boolvalue != value)
               {
                   this.boolvalue = value;
                   this.OnPropertyChanged();
               }
           }
       }
    

    【讨论】:

    • 感谢您的回复,但我认为您错过了问题的全部要点:为什么当视图的新实例(和新视图-模型)已加载?请注意,我不会重新加载旧视图,我每次都会创建一对“新”视图和视图模型,并且此时旧视图应该超出范围(或 GC'ed)。而且我没有说当视图被卸载时调用了setter。我说正在卸载的视图/视图模型上调用 setter。那个视图模型在那个时候应该已经死了。
    • 您对滚动查看器的看法是正确的,没有滚动查看器也会发生这种情况。我更新了问题,但问题的要点仍未得到解答。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-25
    • 1970-01-01
    • 2013-04-05
    相关资源
    最近更新 更多