WPF View 在關閉時將 ViewModel 屬性設置為 null (WPF View sets ViewModel properties to null on closing)


問題描述

WPF View 在關閉時將 ViewModel 屬性設置為 null (WPF View sets ViewModel properties to null on closing)

I have an application where I'm displaying UserControls in a GroupBox.  To display the controls, I'm binding to a property in the ViewModel of the main form, which returns a ViewModel to be displayed.  I've got DataTemplates set up so that the form automatically knows which UserControl/View to use to display each ViewModel.

When I display a different UserControl, I keep the ViewModel of the previous control active, but the Views are discarded automatically by WPF.

The problem that I'm having is that when the view shuts down, any two way bindings to the properties in the ViewModel are immediately set to null, and so when I display the ViewModel again all of the values are just set to null in the UI.

I assume this is because as part of the View closing down it disposes and clears any values in the controls it contains, and since the bindings are in place they propagate down to the ViewModel as well.

DataTemplates in my resources

<DataTemplate DataType="{x:Type vm:HomeViewModel}">
    <vw:HomeView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SettingsViewModel}">
    <vw:SettingsView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:JobListViewModel}">
    <vw:JobListView />
</DataTemplate>

Code used to display user controls

<GroupBox>
    <ContentControl  Content="{Binding Path=RightPanel}" />
</GroupBox>

Example of a control that I'm binding in one of the Views:

    <ComboBox Name="SupervisorDropDown" ItemsSource="{Binding Path=Supervisors}" DisplayMemberPath="sgSupervisor" 
           SelectedValuePath="idSupervisor" SelectedValue="{Binding Path=SelectedSupervisorID}" />

and the relevant ViewModel properties:   

public ObservableCollection<SupervisorsEntity> Supervisors
    {
        get
        {
            return supervisors;
        }
    }

public int? SelectedSupervisorID
{
    get
    {
        return selectedSupervisorID;
    }
    set
    {
        selectedSupervisorID = value;
        this.OnPropertyChanged("SelectedSupervisorID");
    }
}

Any idea on how to stop my Views nulling the values in my ViewModels?  I'm thinking that maybe I need to set the DataContext of the View to null before it closes down, but I'm not sure how to go about that with the way things are currently binding.


參考解法

方法 1:

I've found one possible solution, but I really don't like it.

It turns out the DataContext IS being set to null already, but that doesn't help.  It happens before the property is set to null.  What appears to be happening is that the data bindings aren't being removed before the UserControl/View disposes of itself, and so the null value propagates down when the control is removed.

So when the DataContext changes, if the new context is null then I remove the relevant bindings on the ComboBox, as follows:

private void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    if (e.NewValue == null)
    {
        SupervisorDropDown.ClearValue(ComboBox.SelectedValueProperty);
    }
}

I'm not a big fan of this method, because it means I have to do remember to do it for every databound control I use.  If there was a way I could just have every UserControl just remove their bindings automatically when they close that would be ok, but I can't think of any way to do that.

Another option might be to just restructure my app, so that the Views don't get destroyed until the ViewModels do - this would sidestep the problem entirely.

方法 2:

When I display a different UserControl, I keep the ViewModel of the previous control active, but the Views are discarded automatically by WPF.

The problem that I'm having is that when the view shuts down, any two way bindings to the properties in the ViewModel are immediately set to null, and so when I display the ViewModel again all of the values are just set to null in the UI.

I'm no expert on either WPF or MVVM, but something about this doesn't sound right.  I have trouble believing that WPF disposal of the view is causing your problem.  At the very least, in my limited experience I've never had anything like that happen.  I suspect the culprit is either code in the view-model or the code swaping out which view-model is used for the datacontext.

方法 3:

After trying to stop the null setting by various means, I gave up and instead got it working as follows.  I made the ViewModel read-only before closing its view.  I accomplish this in my ViewModelBase class, where I added a IsReadOnly boolean property.  Then in ViewModelBase.SetProperty() (see below) I ignore any property changes when IsReadOnly is true.

    protected bool SetProperty<T>( ref T backingField, T value, string propertyName )
    {
        var change = !IsReadOnly && !EqualityComparer<T>.Default.Equals( backingField, value );

        if ( change ) {
            backingField = value;
            OnPropertyChanged( propertyName );
        }
        return change;
    }

It seems to be working like this, although I'd still love to know a better solution.

方法 4:

I had the same problem. What worked for me was removing UpdateSourceTrigger=PropertyChanged from my SelectedValueBindings. PropertyChanged UpdateSourceTriggers seem to fire on bound properties of closing views when you use that pattern:

<!--Users DataGrid-->
<DataGrid Grid.Row="0" ItemsSource="{Binding DealsUsersViewSource.View}"
    AutoGenerateColumns="False" CanUserAddRows="True" CanUserDeleteRows="False"
    HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
    <DataGrid.Resources>
        <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#FFC5D6FB"/>
    </DataGrid.Resources>
    <DataGrid.Columns>

          <!--Username Column-->
          <DataGridComboBoxColumn 
            SelectedValueBinding="{Binding Username}" Header="Username" Width="*">
              <DataGridComboBoxColumn.ElementStyle>
                  <Style TargetType="{x:Type ComboBox}">
                      <Setter Property="ItemsSource" Value="{Binding DataContext.DealsUsersCollection.ViewModels,
                          RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
                      <Setter Property="SelectedValuePath" Value="Username"/>
                      <Setter Property="DisplayMemberPath" Value="Username"/>
                  </Style>
              </DataGridComboBoxColumn.ElementStyle>
              <DataGridComboBoxColumn.EditingElementStyle>
                  <Style TargetType="{x:Type ComboBox}">
                      <Setter Property="ItemsSource" Value="{Binding DataContext.BpcsUsers,
                          RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
                      <Setter Property="SelectedValuePath" Value="Description"/>
                      <Setter Property="DisplayMemberPath" Value="Description"/>
                      <Setter Property="IsEditable" Value="True"/>
                  </Style>
              </DataGridComboBoxColumn.EditingElementStyle>
          </DataGridComboBoxColumn>

          <!--Supervisor Column-->
          <DataGridComboBoxColumn 
            SelectedValueBinding="{Binding Supervisor}" Header="Supervisor" Width="*">
              <DataGridComboBoxColumn.ElementStyle>
                  <Style TargetType="{x:Type ComboBox}">
                      <Setter Property="ItemsSource" Value="{Binding DataContext.DealsUsersCollection.ViewModels,
                          RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
                      <Setter Property="SelectedValuePath" Value="Username"/>
                      <Setter Property="DisplayMemberPath" Value="Username"/>
                  </Style>
              </DataGridComboBoxColumn.ElementStyle>
              <DataGridComboBoxColumn.EditingElementStyle>
                  <Style TargetType="{x:Type ComboBox}">
                      <Setter Property="ItemsSource" Value="{Binding DataContext.BpcsUsers,
                          RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
                      <Setter Property="SelectedValuePath" Value="Description"/>
                      <Setter Property="DisplayMemberPath" Value="Description"/>
                      <Setter Property="IsEditable" Value="True"/>
                  </Style>
              </DataGridComboBoxColumn.EditingElementStyle>
          </DataGridComboBoxColumn>

          <!--Plan Moderator Column-->
          <DataGridCheckBoxColumn Binding="{Binding IsPlanModerator}" Header="Plan Moderator?" Width="*"/>

          <!--Planner Column-->
          <DataGridCheckBoxColumn Binding="{Binding IsPlanner}" Header="Planner?" Width="*"/>

    </DataGrid.Columns>
</DataGrid>

Container View:

<!--Pre-defined custom styles-->
<a:BaseView.Resources>

    <DataTemplate DataType="{x:Type vm:WelcomeTabViewModel}">
        <uc:WelcomeTabView/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type vm:UserSecurityViewModel}">
        <uc:UserSecurityView/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type vm:PackItemRegisterViewModel}">
        <uc:PackItemsRegisterView/>
    </DataTemplate>

</a:BaseView.Resources>

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="30"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="30"/>
    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>
        <RowDefinition Height="30"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="30"/>
    </Grid.RowDefinitions>

    <TabPanel Grid.Column="1" Grid.Row="1">
        <TabControl TabStripPlacement="Top" ItemsSource="{Binding TabCollection}" SelectedIndex="{Binding SelectedTabIndex}"
                    DisplayMemberPath="DisplayName" MinWidth="640" MinHeight="480"/>
    </TabPanel>

</Grid>

Container ViewModel:

TabCollection.Add(new WelcomeTabViewModel());
TabCollection.Add(new UserSecurityViewModel(_userService, _bpcsUsersLookup));
TabCollection.Add(new PackItemRegisterViewModel(_packItemService, _itemClassLookup));
SelectedTabIndex = 0;

方法 5:

Set the UpdateSourceTrigger explicit to LostFocus

If the view is closing and sets its data to null, it has no effect on your data in the viewmodel.

<ComboBox Name="SupervisorDropDown" ItemsSource="{Binding Path=Supervisors}" DisplayMemberPath="sgSupervisor" 
SelectedValuePath="idSupervisor" 
SelectedValue="{Binding Path=SelectedSupervisorID, UpdateSourceTrigger=LostFocus}" />

(by Dan FullerDan FullerRB DavidsonHappyNomadRiegardt SteyntroYman)

參考文件

  1. WPF View sets ViewModel properties to null on closing (CC BY-SA 3.0/4.0)

#mvvm #wpf #.net #C#






相關問題

WPF View 在關閉時將 ViewModel 屬性設置為 null (WPF View sets ViewModel properties to null on closing)

在文本框中正確輸入後啟用按鈕 (Button Enable After Correct Input In TextBox)

WPF說數據項不為空時為空 (WPF says that data item is null when it is not null)

MVVM - 如何將 ViewModel 包裝在 ViewModel 中? (MVVM - How to wrap ViewModel in a ViewModel?)

關於服務參考和 MVVM 模式的幾個一般問題 (A few general questions about Service Reference and MVVM pattern)

WPF MVVM 鏈接視圖 (WPF MVVM Linked Views)

wpf 樹視圖 mvvm (wpf treeview mvvm)

如何在 MVVM Light 的 ListView 中的 ComboBox 中顯示列表? (How to show a List in a ComboBox in a ListView in MVVM Light?)

多次調用 PropertyChanged 的 ViewModel 屬性 (ViewModel properties with multiple calls to PropertyChanged)

如何將圖像存儲在類庫中並從任何類訪問它 (How can i store an image in a class library and access it from any class)

Silverlight MVVM 隔離存儲 (Silverlight MVVM Isolated Storage)

如何將文本框的borderBrush屬性綁定到viewmodel中的屬性,類型轉換錯誤 (How to bind the borderBrush property of a textbox to a property in viewmodel, type conversion error)







留言討論