-1

I have a WPF MVVM app, which gets its data from a user setting which is an ObservableCollection of type Copyable (a custom class) called Copyables. Within the main view model (ClipboardAssistantViewModel), I set the source of a CollectionViewSource to Copyables. This is then bound to an ItemsControl in the main view (MainWindow). The DataTemplate for this ItemsControl is a user control, 'CopyableControl', which is essentially a button, but with attached properties that allow me to bind data and commands to it.

When a user clicks on a CopyableControl, a view model (DefineCopyableViewModel) is added to an ObservableCollection of those in ClipboardAssistantViewModel, and that collection is bound to an ItemsControl in MainWindow. The DataTemplate of this is a UserControl called DefineCopyableControl, which is set up in such a way that the current values associated with the clicked Copyable are bound to textboxes in the DefineCopyableControl for editing.

My problem: There is a method in DefineCopyableViewModel, EditCopyable(), which only works on the first run (its job is to save the user settings once any edits have taken place and the user clicks "OK"). If I click the CopyableControl and make an edit, then click "OK", then click it again, make another edit, then click "OK", then close the application and open it again, only the first edit has been saved (even though the UI was updated with the edited value both times). It seems to have something to do with the data-binding need to be "refreshed"; please see the comments in this method in the code for my findings around this.

My code is as follows:

Model:

namespace ClipboardAssistant.Models
{
    public class Copyable : INotifyPropertyChanged
    {
        // INotifyPropertyChanged implementation

        private string name;
        public string Name
        {
            get { return name; }
            set
            {
                if (value != name)
                {
                    name = value;
                    NotifyPropertyChanged("Name");
                }
            }
        }

        private string textToCopy;
        public string TextToCopy
        {
            get { return textToCopy; }
            set
            {
                if (value != textToCopy)
                {
                    textToCopy = value;
                    NotifyPropertyChanged("TextToCopy");
                }
            }
        }

        public Copyable() { }

        public Copyable(string Name, string TextToCopy)
        {
            this.Name = Name;
            this.TextToCopy = TextToCopy;
        }
    }
}

ViewModels:

namespace ClipboardAssistant.ViewModels
{
    public class ClipboardAssistantViewModel : INotifyPropertyChanged
    {
        // INotifyPropertyChanged Implementation

        public CollectionViewSource CopyablesView { get; set; }

        public ObservableCollection<DefineCopyableViewModel> Definers { get; set; }

        public CopyableClickCommand CopyableClickCommand { get; set; }

        public ClipboardAssistantViewModel()
        {
            Definers = new ObservableCollection<DefineCopyableViewModel>();

            CopyablesView = new CollectionViewSource();
            CopyablesView.Source = Properties.Settings.Default.Copyables;

            CopyableClickCommand = new CopyableClickCommand(this);
            EditModeClickCommand = new EditModeClickCommand(this);
        }

        public void RefreshCopyables()
        {
            // Both these methods of refreshing appear to have the same effect.
            Properties.Settings.Default.Copyables = (ObservableCollection<Copyable>)CopyablesView.Source;
            CopyablesView.Source = Properties.Settings.Default.Copyables;
        }

        public void EditCopyable(Copyable Copyable)
        {
            Definers.Add(new DefineCopyableViewModel(Copyable, this));
        }
    }
}

namespace ClipboardAssistant.ViewModels
{
    public class DefineCopyableViewModel : INotifyPropertyChanged
    {
        // INotifyPropertyChanged Implementation

        public ClipboardAssistantViewModel MyParent { get; set; }

        public Copyable Copyable { get; set; }

        public DefinerOKClickCommand DefinerOKClickCommand { get; set; }

        public DefineCopyableViewModel(Copyable Copyable, ClipboardAssistantViewModel MyParent)
        {
            this.Copyable = Copyable;
            this.MyParent = MyParent;

            DefinerOKClickCommand = new DefinerOKClickCommand(this);
        }

        public void EditCopyable()
        {
            // Refresh, save, no refresh, save -> doesn't save second edit.

            // Save, refresh, save, no refresh -> does save second edit.

            MessageBoxResult r = MessageBox.Show("Refresh?", "Refresh", MessageBoxButton.YesNo);
            if (r == MessageBoxResult.Yes)
            {
                MyParent.RefreshCopyables();
            }

            // These two MessageBox methods (save and refresh) can be swapped around (see above comments).

            MessageBoxResult s = MessageBox.Show("Save?", "Save", MessageBoxButton.YesNo);
            if (s == MessageBoxResult.Yes)
            {
                Properties.Settings.Default.Save();
            }

            MyParent.Definers.Remove(this);
        }
    }
}

MainWindow:

<Window x:Class="ClipboardAssistant.Views.MainWindow" x:Name="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:vm="clr-namespace:ClipboardAssistant.ViewModels"
                xmlns:ctrls="clr-namespace:ClipboardAssistant.Controls"
                mc:Ignorable="d"
                Title="Clipboard Assistant" Height="400" Width="700">
    <Window.DataContext>
        <vm:ClipboardAssistantViewModel />
    </Window.DataContext>

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

            <ItemsControl ItemsSource="{Binding CopyablesView.View}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel Orientation="Vertical" />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>

                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <ctrls:CopyableControl Copyable="{Binding}"
                                               ClickCopyable="{Binding DataContext.CopyableClickCommand, ElementName=mainWindow}" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>

            <DockPanel Grid.Row="2">
                <Button x:Name="btnEditCopyableMode" HorizontalAlignment="Left" DockPanel.Dock="Left"
                        Content="Edit" Margin="0,0,10,0" Command="{Binding EditModeClickCommand}" />
            </DockPanel>
        </Grid>

        <ItemsControl ItemsSource="{Binding Definers}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Grid />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>

            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <ctrls:DefineCopyableControl Copyable="{Binding DataContext.Copyable}"
                                                 ClickCancel="{Binding DataContext.DefinerCancelClickCommand, ElementName=mainWindow}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

CopyableControl:

<UserControl x:Class="ClipboardAssistant.Controls.CopyableControl" x:Name="copyableControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:ClipboardAssistant.Controls"
             mc:Ignorable="d" d:DesignHeight="75" d:DesignWidth="200">
    <Grid Width="200" Height="75">
            <Button Command="{Binding ClickCopyable, ElementName=copyableControl}"
                CommandParameter="{Binding Copyable, ElementName=copyableControl}"
                Content="{Binding Copyable.Name, ElementName=copyableControl}"
                Style="{StaticResource CopyableMainButtonStyle}" />
    </Grid>
</UserControl>
namespace ClipboardAssistant.Controls
{
    public partial class CopyableControl : UserControl
    {
        public static readonly DependencyProperty ClickCopyableProperty =
            DependencyProperty.Register("ClickCopyable", typeof(ICommand), typeof(CopyableControl));

        public ICommand ClickCopyable
        {
            get { return (ICommand)GetValue(ClickCopyableProperty); }
            set { SetValue(ClickCopyableProperty, value); }
        }

        public static readonly DependencyProperty CopyableProperty =
            DependencyProperty.Register("Copyable", typeof(Copyable), typeof(CopyableControl));

        public Copyable Copyable
        {
            get { return (Copyable)GetValue(CopyableProperty); }
            set { SetValue(CopyableProperty, value); }
        }

        public CopyableControl()
        {
            InitializeComponent();
        }
    }
}

DefineCopyableControl:

<UserControl x:Class="ClipboardAssistant.Controls.DefineCopyableControl" x:Name="defineCopyableControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="500">
    <Grid x:Name="MainGrid" Background="Blue">
        <Grid Width="200" Height="180">
            <Grid.RowDefinitions>
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
                <RowDefinition Height="10" />
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
                <RowDefinition Height="20" />
                <RowDefinition Height="30" />
            </Grid.RowDefinitions>

            <Label Grid.Row="0" Content="Name" Foreground="White" />
            <TextBox Grid.Row="1" Text="{Binding Copyable.Name}" x:Name="tbN" />

            <Label Grid.Row="3" Content="Copyable Text" Foreground="White" />
            <TextBox Grid.Row="4" Text="{Binding Copyable.TextToCopy}" x:Name="tbTTC" />

            <DockPanel Grid.Row="6">
                <Button Width="70" Content="OK" DockPanel.Dock="Right" HorizontalAlignment="Right"
                        Command="{Binding DefinerOKClickCommand}"
                        CommandParameter="{Binding ElementName=defineCopyableControl}" />
            </DockPanel>
        </Grid>
    </Grid>
</UserControl>
public partial class DefineCopyableControl : UserControl
{
    public static readonly DependencyProperty CopyableProperty =
        DependencyProperty.Register("Copyable", typeof(Copyable), typeof(DefineCopyableControl));

    public Copyable Copyable
    {
        get { return (Copyable)GetValue(CopyableProperty); }
        set { SetValue(CopyableProperty, value); }
    }

    public DefineCopyableControl()
    {
        InitializeComponent();
    }
}

Commands:

namespace ClipboardAssistant.ViewModels.Commands
{
    public class CopyableClickCommand : ICommand
    {
        public ClipboardAssistantViewModel ViewModel { get; set; }

        public CopyableClickCommand(ClipboardAssistantViewModel viewModel)
        {
            ViewModel = viewModel;
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public void Execute(object parameter)
        {
            Copyable cp = (Copyable)parameter;

            ViewModel.EditCopyable(cp);
        }
    }
}

namespace ClipboardAssistant.ViewModels.Commands
{
    public class DefinerOKClickCommand : ICommand
    {
        public DefineCopyableViewModel ViewModel { get; set; }

        public DefinerOKClickCommand(DefineCopyableViewModel viewModel)
        {
            ViewModel = viewModel;
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public void Execute(object parameter)
        {
            ViewModel.EditCopyable();
        }
    }
}

Settings:

namespace ClipboardAssistant.Properties {


    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")]
    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {

        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));

        public static Settings Default {
            get {
                return defaultInstance;
            }
        }

        [global::System.Configuration.UserScopedSettingAttribute()]
        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
        public global::System.Collections.ObjectModel.ObservableCollection<ClipboardAssistant.Models.Copyable> Copyables {
            get {
                return ((global::System.Collections.ObjectModel.ObservableCollection<ClipboardAssistant.Models.Copyable>)(this["Copyables"]));
            }
            set {
                this["Copyables"] = value;
            }
        }
    }
}
Chris Mack
  • 5,148
  • 2
  • 12
  • 29
  • You might find someone willing to tackle this, but speaking for myself: I know what a pain it is to convince the designer-provided `Settings` object to store something like `ObservableCollection` and would not spend any time investigating a question that requires me to do so. Your code example is incomplete without a straight copy/paste implementation of the settings. Additionally, I seriously doubt that all of the above is really required to reproduce the issue; your code example also appears not to be minimal. – Peter Duniho Aug 09 '16 at 22:46
  • I have added the Settings into the code example. – Chris Mack Aug 10 '16 at 17:55

1 Answers1

1

I'm assuming you are using Visual Studio. In that case, in the My Project do you have the settings listed in the settings tab?

I ran into the same issue a while back where I tried to programatically create/save/update settings and was unsucessful until I created the setting in the Settings tab. Once that was complete I was able to make my saves as necessary.

The you just use

MySettings.Default.SettingName = value
MySettings.Default.Save()

Hope this helps!

Dustin
  • 216
  • 1
  • 9
  • The collection is in the Settings tab, although I had to do something like this - [http://stackoverflow.com/questions/2890271/how-to-save-a-liststring-on-settings-default](http://stackoverflow.com/questions/2890271/how-to-save-a-liststring-on-settings-default) - to get the Settings tab to allow me to select the ObservableCollection as a valid type (I had to edit the xml file). Also, I don't want to have to set the actual settings to any value in my method, as the data-binding should be taking care of that (it does on the first run). – Chris Mack Aug 09 '16 at 21:12