Combox SelectedItem does not apply when restoring

2019-03-03 10:21发布

问题:

I'm facing a strange problem when using C# WPF and MVVM Pattern while restoring a ViewModel (serialized using Json.Net).

The idea of the software is - when closing the window - to persist the current Viewmodel state in a json file.

At the next startup the app just serarches for the json.

  • If there a file, then deserialize it and restore the ViewModel (set public properties).
  • If there is no file, then the viewmodel is created and default values are set.

Now my problem is, that when restoring it with the json file, a combobox containing a list of a custom type, the combobox has values but no SelectedItem. When creating the viewmodel instance and initiailizing the public properties with default values (doing this via the code behind) then everything is fine.

Here is some code that represents the "error": View

<Window x:Class="CrazyWpf.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:CrazyWpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        Closing="Window_Closing"
        Loaded="Window_Loaded">
    <StackPanel
        x:Name="rootElement"
        Orientation="Vertical"
        HorizontalAlignment="Left"
        VerticalAlignment="Top"
        Margin="10">
        <StackPanel.DataContext>
            <local:DemoViewModel />
        </StackPanel.DataContext>
        <StackPanel
            Orientation="Horizontal">
            <Label
                x:Name="lblID"
                Width="30"
                Content="ID:"/>
            <TextBox
                x:Name="tbID"
                Width="50"
                Margin="30,0,0,0"
                Text="{Binding ID, UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>
        <StackPanel
            Orientation="Horizontal">
            <Label
                x:Name="lblName"
                Width="45"
                Content="Name:"/>
            <TextBox
                x:Name="tbName"
                Width="200"
                Margin="15,0,0,0"
                Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>
        <StackPanel
            Orientation="Horizontal">
            <Label
                x:Name="lblStai"
                Width="60"
                Content="Status:"/>
            <ComboBox
                x:Name="cbStati"
                Width="200"
                ItemsSource="{Binding StatusTypeList}"
                SelectedItem="{Binding StatusType, UpdateSourceTrigger=PropertyChanged}"
                DisplayMemberPath="Name"/>
        </StackPanel>
    </StackPanel>
</Window>

Code Behind

using System;
using System.Windows;
using System.IO;

using Newtonsoft.Json;

namespace CrazyWpf
{
    public partial class MainWindow : Window
    {
        private DemoViewModel dvm;
        public MainWindow()
        {
            InitializeComponent();

            this.dvm = (DemoViewModel)this.rootElement.DataContext;
        }

        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            string filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "settings.json");
            if (File.Exists(filePath))
                File.Delete(filePath);

            File.WriteAllText(filePath, JsonConvert.SerializeObject(this.dvm, Formatting.Indented));
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            string filePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "settings.json");
            if (!File.Exists(filePath))
            { this.SetDefaultSettings(); return; }

            DemoViewModel d = JsonConvert.DeserializeObject<DemoViewModel>(File.ReadAllText(filePath));
            this.dvm.ID = d.ID;
            this.dvm.Name = d.Name;
            this.dvm.StatusType = d.StatusType;

        }

    }
}

BaseViewModel:

using System.ComponentModel;

namespace CrazyWpf
{
    public abstract class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

ViewModel

using System;
using System.Collections.Generic;
using System.Linq;

using Newtonsoft.Json;

namespace CrazyWpf
{
    class DemoViewModel : BaseViewModel
    {
        [JsonIgnore]
        private int id;
        [JsonProperty(Order = 1)]
        public int ID
        {
            get { return this.id; }
            set
            {
                if (this.id != value)
                {
                    this.id = value;
                    this.OnPropertyChanged("ID");
                }
            }
        }

        [JsonIgnore]
        private string name;
        [JsonProperty(Order = 2)]
        public string Name
        {
            get { return this.name; }
            set
            {
                if (this.name != value && value != null)
                {
                    this.name = value;
                    this.OnPropertyChanged("Name");
                }
            }
        }

        [JsonIgnore]
        private StatusTyp statusType;
        [JsonProperty(Order = 3)]
        public StatusTyp StatusType
        {
            get { return this.statusType; }
            set
            {
                if (this.statusType != value && value != null)
                {
                    this.statusType = value;
                    this.OnPropertyChanged("StatusType");
                }
            }
        }

        [JsonIgnore]
        private List<StatusTyp> statusTypeList;
        [JsonProperty(Order = 4)]
        public List<StatusTyp> StatusTypeList
        {
            get { return this.statusTypeList; }
            set
            {
                if (this.statusTypeList != value && value != null)
                {
                    this.statusTypeList = value;
                    this.OnPropertyChanged("StatusTypeList");
                }
            }
        }

        public DemoViewModel()
        {
            this.StatusTypeList = new Func<List<StatusTyp>>(() =>
            {
                var list = Enum.GetValues(typeof(Status))
                    .Cast<Status>()
                    .ToDictionary(k => (int)k, v => v.ToString())
                    .Select(e => new StatusTyp()
                    {
                        Value = e.Key,
                        Name = e.Value,
                        Status =
                            Enum.GetValues(typeof(Status))
                                .Cast<Status>().
                                Where(x =>
                                {
                                    return (int)x == e.Key;
                                }).FirstOrDefault()
                    })
                    .ToList();
                return list;
            })();
        }
    }

    public class StatusTyp
    {
        public int Value { get; set; }
        public string Name { get; set; }
        public Status Status { get; set; }
    }

    public enum Status
    {
        NotDetermined = 0,
        Determined = 1,
        Undeterminded = 2,
        Unknown = 3
    }
}

回答1:

If you have an ItemsSource and a SelectedItem, the instance in SelectedItem MUST BE in the collection bound to ItemsSource. If it is not, then your bindings will not work as expected.

The control uses reference equality to determine which item in ItemsSource is the one in SelectedItem and update the UI. This normally isn't a problem as the control populates SelectedItem for you, but if you are updating from the ViewModel side, you have to make sure your references are managed correctly.

This can be an issue when serializing/deserializing your view model. Most common serializers don't track references, and so cannot restore these on deserialization. The same object may be referenced multiple places in the original object graph, but after deserialization you now have multiple instances of the original spread throughout the rehydrated graph. This won't work with your requirements.

What you have to do is, after deserializing, find the matching instance in your collection and substitute it for the instance in SelectedItem. Or, use a serializer that tracks instances.. The XAML serializer already does this, and is a surprisingly good xml serializer for .net object graphs.