创建新视图时在未加载的视图/视图模型上绑定引发

时间:2016-11-13 06:32:52

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

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

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)定义UserControl(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)在视图后面添加代码:

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)MainPage(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)::已卸载

     

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

     

查看(58892413):: ctor

     

ViewModel(61646925):: ctor

     

ViewModel(61646925):: Value :: get

     

ViewModel(59312528):: Value :: set

     

查看(58892413)::已加载

     

关闭弹出窗口......

     

查看(58892413)::已卸载

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

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

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

1 个答案:

答案 0 :(得分:0)

  

这意味着在Unloaded视图中创建的ViewModel的setter   模型被称为有点奇怪

首先,在卸载视图时不调用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上选择setter之前不会调用rdo1。或者,您可以使用其他控件CheckBox进行测试,在更新setter值之前也不会调用IsChecked

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;       
    }
}
  

如果从View中删除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();
           }
       }
   }