anakic/Jot

Cannot save controls from Pages or other Windows

Coke21 opened this issue · 13 comments

I'm saving controls from MainWindow (and it works)

        public MainWindow()
        {
            InitializeComponent();

            Persistence.Tracker.Configure<MainWindow>()
                .Property(p => p.ThemeToggleSwitch.IsChecked, true, "Theme")
                .Property(p => p.RunAtStartUpCheckBox.IsChecked, false, "Run at startup")
                .Property(p => p.DefaultTimeZoneCheckBox.IsChecked, false, "timezone checkbox")
                .Property(p => p.TimeZoneIntegerUpDown.Value, 0, "timezone value");

            Persistence.Tracker.Track(this);
        }

My Persistence class:

    class Persistence
    {
        public static Tracker Tracker = new Tracker();

        static Persistence()
        {
            Tracker.Configure<Window>()
                .Id(w => w.Name)
                .Properties(w => new {w.Height, w.Width, w.Top, w.Left })
                .PersistOn(nameof(Window.Closing))
                .StopTrackingOn(nameof(Window.Closing));
        }
    }

I have a few Pages and an Window, therefore, I want to save controls from them. However, I cannot do it with this:

        public MainWindow()
        {
            InitializeComponent();

            //Persistence.Tracker.Configure<PageBattleMetrics>()
            //    .Property(p => p.NotifyCheckbox.IsChecked, false, "Notify checkbox");

            //Persistence.Tracker.Track(Data.PageBattleMetrics);

            Persistence.Tracker.Configure<MainWindow>()
                .Property(p => p.ThemeToggleSwitch.IsChecked, true, "Theme")
                .Property(p => p.RunAtStartUpCheckBox.IsChecked, false, "Run at startup")
                .Property(p => p.DefaultTimeZoneCheckBox.IsChecked, false, "timezone checkbox")
                .Property(p => p.TimeZoneIntegerUpDown.Value, 0, "timezone value");

            Persistence.Tracker.Track(this);
        }

Any idea what I should do to also save controls from other Pages and Windows?

What issue are you running into?

One issue might be if you have multiple instances of PageBattleMetrics. In that case, you should make sure to identify them by some property, e.g. .Configure<PageBattleMetrics>().Id(pbm=>pbm.Name)

Thank you for the response,
There's only 1 instance of PageBattleMetrics (In Data class):
public static PageBattleMetrics PageBattleMetrics { get; set; } = new PageBattleMetrics();
The code looks like this now:

        public MainWindow()
        {
            InitializeComponent();

            Persistence.Tracker.Configure<PageBattleMetrics>()
                .Id(p => p.Name)
                .Property(p => p.NotifyCheckbox.IsChecked, false, "Notify checkbox");

            Persistence.Tracker.Track(Data.PageBattleMetrics);

            Persistence.Tracker.Configure<MainWindow>()
                .Property(p => p.ThemeToggleSwitch.IsChecked, true, "Theme")
                .Property(p => p.RunAtStartUpCheckBox.IsChecked, false, "Run at startup")
                .Property(p => p.DefaultTimeZoneCheckBox.IsChecked, false, "timezone checkbox")
                .Property(p => p.TimeZoneIntegerUpDown.Value, 0, "timezone value");

            Persistence.Tracker.Track(this);
        }

But it still doesn't work. The program doesn't crash, I "check" the checkbox and then close the app but when I relaunch it, the checkbox doesn't get checked (Basically it doesn't save).
My folder in Roaming looks like this:
https://gyazo.com/6160718b2809822440caff941cb70e80
In this json file:
https://gyazo.com/e756fc2470be9a83bf5cb891e9dbcf31

Ok I edited the code a bit.
In Persistence class:
public static Tracker Tracker = new Tracker();

In MainWindow(works):

        public MainWindow()
        {
            InitializeComponent();

            Persistence.Tracker.Configure<MainWindow>()
                .Id(p => p.Name)
                .Properties(p => new { p.Height, p.Width, p.Top, p.Left })
                .Property(p => p.ThemeToggleSwitch.IsChecked, false, "Theme")
                .Property(p => p.RunAtStartUpCheckBox.IsChecked, false, "Run at startup")
                .Property(p => p.DefaultTimeZoneCheckBox.IsChecked, false, "timezone checkbox")
                .Property(p => p.TimeZoneIntegerUpDown.Value, 0, "timezone value")
                .PersistOn(nameof(Closing))
                .StopTrackingOn(nameof(Closing));

            Persistence.Tracker.Track(this);
        }

In PageBattleMetrics constructor:

        public PageBattleMetrics()
        {
            InitializeComponent();

            Persistence.Tracker.Configure<PageBattleMetrics>()
                .Id(p => p.Name)
                .Property(p => p.NotifyCheckbox.IsChecked, false, "Notify checkbox")
                .PersistOn(nameof(MainWindow.Closing))
                .StopTrackingOn(nameof(MainWindow.Closing));

            Persistence.Tracker.Track(this);
        }

The above throws an exception on
Persistence.Tracker.Track(this);
System.NullReferenceException: 'Object reference not set to an instance of an object.'

Maybe the Page is not fully initialized and that's why it is causing the problems?

In the PageBattleMetrics constructor, it can't find "Closing" events on the PageBattleMetrics class, that's the cause of the exception. The exception message could be more helpful, though. I've added a better exception message but will push when I have a bit more time.

You should use the checkbox events to trigger persisting. Also, make sure that your PageBattleMetrics instance has a Name. If neither the MainWindow nor the PageBattleMetrics instances have a name, Jot will treat them as the same object. You can either give them each a different name or include their types as the second parameter in the Id() method.

In your case, the code below should work:

        public PageBattleMetrics()
        {
            InitializeComponent();

            Persistence.Tracker.Configure<PageBattleMetrics>()
                .Id(p => p.Name, p.GetType())
                .Property(p => p.NotifyCheckbox.IsChecked, false, "Notify checkbox")
                .PersistOn(nameof(CheckBox.Checked), this.NotifyCheckbox) // use the checkbox as the source of the event
                .PersistOn(nameof(CheckBox.Unchecked), this.NotifyCheckbox);

            Persistence.Tracker.Track(this);
        }

Also, if you're using the MVVM approach, consider tracking the VM instead of the Window and other UI elements. It might make things simpler, especially if you have multiple parts of the UI binding to the same view-model. Not sure if it will help here, but it's something to consider.

Wow, this actually worked! Also, naming the user controls really helped. Now my folder looks like this:
https://gyazo.com/5facba02466ab71eab95f92c52a79169
Instead of just .json file. Really helps the structure.
At the moment, the program is not using MVVM approach but it's inevitable if I want to progress.
Thank you again for your help!

I have just one last question, how do I save the items in ListView?
In XAML:
<GridViewColumn Header="Parameter(s):" Width="254" DisplayMemberBinding="{Binding Parameter}"/>

Then:

        public class LvItem
        {
            public string Parameter { get; set; }
        }
        public  ObservableCollection<LvItem> Items { get; set; } = new ObservableCollection<LvItem>();

And Add:
Items.Add(new LvItem() { Parameter = "bla bla" });

I used to save it as StringCollection in Properties.Settings. How would I do it with your library?

Just track the Items property like any other.

You might have to persist on button click or whatever event is appropriate (what ever event handler modifies the Items collection). Keep in mind that when Jot is applying previously saved data, it will replace your instance of ObservableCollection with a new one (that it deserialized from the .json file) so you cannot use your instance's CollectionChanged event as a persist trigger.

In any case, I'd definitely suggest moving to MVVM, it makes working with WPF a lot easier.

Thank you that worked. How would I go about making it automatic so if I relaunch the app, the listview would be refreshed (items added)?
Currently, I have this:

        public PageBattleMetrics()
        {
            InitializeComponent();

            Persistence.Tracker.Configure<PageBattleMetrics>()
                .Id(p => p.Name)

                .Properties(p => new {p.Items})
                //.Property(p => p.Items)                                             
                .PersistOn(nameof(LvName.PreviewDrop), LvName)
                .PersistOn(nameof(LvName.PreviewKeyDown), LvName)
        }

The items are added when I drop another item on the listview (they are pretty much there but just not showing). Is there any events that would help me achieve this?

You're missing a call to Persistence.Tracker.Track(this); in the constructor from what I can tell. Without seeing the rest of the code I can't tell if there are any other issues.

Yes, sorry I just didn't paste it in the above code (It's originally in my code). When I put items e.g. item1, item2 etc in my listview then I close my app. I can see that your library made changes to the .json file. In there, I can see item1, item2. I relaunch the app and the items don't appear in the listview (it's an empty list). However, when I add another item e.g. item3 then the whole list refreshes and item1, item2 appear.
That's the whole constructor:

        public PageBattleMetrics()
        {
            InitializeComponent();

            Persistence.Tracker.Configure<PageBattleMetrics>()
                .Id(p => p.Name)

                .Property(p => p.NotifyCheckbox.IsChecked, false, "Notify checkbox")
                .PersistOn(nameof(CheckBox.Checked), NotifyCheckbox)
                .PersistOn(nameof(CheckBox.Unchecked), NotifyCheckbox)

                .Property(p => p.IgnoreResponse.Value, 0, "Ignore response value")
                .PersistOn(nameof(NumericUpDown.ValueChanged), IgnoreResponse)

                .Properties(p => new {p.Items})
                //.Property(p => p.Items)                                             
                .PersistOn(nameof(LvName.PreviewDrop), LvName)
                .PersistOn(nameof(LvName.PreviewKeyDown), LvName)

                .Property(p => p.CheckBoxResponse.IsChecked, false, "Run constant checkbox")
                .PersistOn(nameof(CheckBox.Checked), CheckBoxResponse)
                .PersistOn(nameof(CheckBox.Unchecked), CheckBoxResponse);
                
            Persistence.Tracker.Track(this);
        }

That part looks fine. How are you feeding the Items to the listview? Is your project on GitHub?

Hi, sorry for the waiting time. Here's everything about this listview:
XAML:

                   <ListView x:Name="LvName" PreviewKeyDown ="LvItemHotKey" Height="100"
                              AllowDrop="True" PreviewDrop="PathDropLv">

                        <ListView.ItemContainerStyle>
                            <Style TargetType="ListViewItem">
                                <EventSetter Event="MouseEnter" Handler="LvItemMouseEnter"/>

                                <Setter Property="HorizontalContentAlignment" Value="Center"/>
                                <Setter Property="FontWeight" Value="Bold"/>
                                <Setter Property="BorderBrush" Value="Black" />

                                <Style.Triggers>
                                    <Trigger Property="IsMouseOver" Value="true">
                                        <Setter Property="Background" Value="LightGreen"/>
                                        <Setter Property="Foreground" Value="Blue"/>
                                    </Trigger>
                                    <Trigger Property="IsSelected" Value="true">
                                        <Setter Property="Background" Value="Red"/>
                                        <Setter Property="Foreground" Value="White"/>
                                    </Trigger>
                                </Style.Triggers>
                            </Style>
                        </ListView.ItemContainerStyle>

                        <ListView.ContextMenu>
                            <ContextMenu Name="LvContextMenuName">
                                <MenuItem Header="Add" Click="AddParameter_Click" />
                                <MenuItem Header="Copy" Click="CopyParameter_Click" />
                                <MenuItem Header="Delete" Click="DeleteParameter_Click" />
                            </ContextMenu>
                        </ListView.ContextMenu>

                        <ListView.View>
                            <GridView>
                                <GridView.ColumnHeaderContainerStyle>
                                    <Style TargetType="{x:Type GridViewColumnHeader}">
                                        <Setter Property="Background" Value="{x:Null}" />

                                        <Style.Triggers>
                                            <Trigger Property="IsMouseOver" Value="true">
                                                <Setter Property="Foreground" Value="Black"/>
                                            </Trigger>
                                        </Style.Triggers>

                                    </Style>
                                </GridView.ColumnHeaderContainerStyle>

                                <GridViewColumn Header="Parameter(s):" Width="254" DisplayMemberBinding="{Binding Parameter}"/>
                            </GridView>
                        </ListView.View>
                    </ListView>

Then in code:

        //ListView
        private void AddParameter_Click(object sender, RoutedEventArgs e) => MenuItemClick("addParameter");
        private void CopyParameter_Click(object sender, RoutedEventArgs e) => MenuItemClick("copyParameter");
        private void DeleteParameter_Click(object sender, RoutedEventArgs e) => MenuItemClick("deleteParameter");

        public class LvItem
        {
            public string Parameter { get; set; }
        }
        public  ObservableCollection<LvItem> Items { get; set; } = new ObservableCollection<LvItem>();
        private  LvItem CurrentItem { get; set; }
        public  void MenuItemClick(string menuItem)
        {
            LvItem selectedItem = (LvItem)Data.PageBattleMetrics.LvName.SelectedItem;
            switch (menuItem)
            {
                case "addParameter":
                    if (Clipboard.GetText() == "") return;

                    if (!Items.Any(item => item.Parameter == Clipboard.GetText()))
                    {
                        if (Clipboard.GetText().Length > 0)
                        {
                            Items.Add(new LvItem() { Parameter = $"{Clipboard.GetText()}" });
                            Data.PageBattleMetrics.LvName.ItemsSource = Items;
                        }
                    }
                    else
                        MessageBox.Show($"The '{Clipboard.GetText()}' parameter is already in the list!", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
                    break;
                case "copyParameter":
                    if (selectedItem != null)
                        Clipboard.SetText(selectedItem.Parameter);
                    else
                        MessageBox.Show("The parameter wasn't selected!", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
                    break;
                case "deleteParameter":
                    if (selectedItem != null)
                        Items.Remove(selectedItem);
                    else
                        MessageBox.Show("The parameter wasn't selected!", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
                    break;
            }
        }
        //Mouse Events
        public void LvItemMouseEnter(object sender, MouseEventArgs e)
        {
            LvName.Focus();
            var item = sender as ListViewItem;
            CurrentItem = (LvItem)item.Content;
        }
        //On drop on listview
        public void PathDropLv(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.Text))
            {
                Clipboard.SetText((string)e.Data.GetData(DataFormats.Text));
                MenuItemClick("addParameter");
            }
        }
        //Hotkey
        public void LvItemHotKey(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.C && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
                Clipboard.SetText(CurrentItem.Parameter);

            if (e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
                MenuItemClick("addParameter");

            if (e.Key == Key.Delete)
                if (CurrentItem != null)
                    Items.Remove(CurrentItem);
        }

A user can either copy and paste it in the listview or drag&drop on it.
Thank you again for the time you spend on it.

Alright, I've found the fix. It was really simple. You have to add
LvName.ItemsSource = Items;
In the constructor of that page. So all my issues are now fixed. Thank you so much for your help.

Happy to help