Search code examples
c#.net-6.0winui-3navigationviewwindows-app-sdk

WinUI (C#): How To Share Data Between Pages - The Proper Way?


I'm very new to this, especially WinUI, so please bear with me.

I'm writing an UI app with Visual Studio which communicates with a console program through a JSON file with properties. The UI has two main sections and a setting page. The pages share parts of the properties which will have to be written to the JSON file. I think the NavigationView is perfect for my purpose. In any case, I want to avoid having everything in one window.

This is a new version of a Windows Forms app, which had very few controls and only a main window with menu elements.

  1. I tried to add a similar way by reading properties/variables from other pages.
  • Using static objects works but is a lot of manual work to hook up with the controls.
  • Similarly, such a solution requires a lot of manual work as well.
  • Using two instances doesn't work as each instance seems to update separately.
  1. Since static objects work, I tried to bind controls to static properties.
  • I couldn't find a proper guide for WinUI, so I tried to use something like this.
  • Again, making instances in any form (xaml, code-behind) and update that, never seems to update the source property.
  • I also tried to use non-static properties and make an instance of it on other pages, which to my biggest surprise didn't work either (Maybe I didn't do this correctly?).
  1. There are a number of Stackoverflow questions about this, most of them didn't work out for me (some details in things I tried).
  • This MAUI answer recommends DependencyService, but it seems to be exclusive to Xamarin (and MAUI seems to be different as well).
  • A number of solutions turned out to be exclusive for other app styles (forms, framework, etc). I wasn't able to translate any of these methods to my WinUI app. (Example)
  1. A popular way to use properties seems to be a MVVM.
  • This works on the same page only. It looks like the instances do not update the source either.
  • The previously mentioned solution with static objects didn't work either.
  • I also tried the toolkit MVVM package, trying to copy what the Template Studio for WinUI does, but it was no different.
  • There seems to be ways to share data between ViewModels and even pages (by using their class as models) with a Main ViewModel (link). I don't know how to do that and if it works.
  1. The Template Studio seems to be saving and reading from a file. However, I am concerned about performance since that would constantly read from and write to disk.
  • The only implemented setting in the Template Studio (on only one page), the theme changer writes to yet another file which I must avoid. (I will have a UI settings file but it must be editable with a text editor.)
  • Writing settings to disk would probably mean that I have to add a command for each control, so I can just as well use a method from 1.
  • There seems to be a way to bind settings files (various formats like INI, XML, JSON), which I haven't checked out more closely, as I am concerned about performance.
  1. I saw some other suggestions, such as:
  • DynamicMethod, which seems to have poor performance and doesn't seem to solve my issue.
  • Dependency properties which seem to have poor performance as well (seems to be better now). I couldn't get this to work, probably because I couldn't find a good tutorial. It seems to be a promising solution, so if this is the recommended way, I can provide my non-working code.

Examples:

1. (static properties require a lot of manual assignment)

Page1 (xaml)

                <ToggleSwitch x:Name="test" Header="test switch" OffContent="Off" OnContent="On" Toggled="test_Toggled"/>

Page1 code-behind (C#)

[...]
        public static MyClass Test { get; set; }
[...]
        private void test_Toggled(object sender, RoutedEventArgs e)
        {
            Test.TestSwitch = test.IsOn;
        }
[...]

Page2 code-behind (C#)

using static Namespace.Page1;
[...]
            TextBox.Text = Test.TestSwitch.ToString();
[...]

2. (couldn't figure out proper static binding and using non-static properties doesn't work for me)

Page1 (xaml)

                <ToggleSwitch x:Name="test" Header="test switch" IsOn="{x:Bind Test.TestSwitch, Mode=TwoWay}" OffContent="Off" OnContent="On"/>
                <TextBox x:Name="test2" Text="{x:Bind Test.TestSwitch, Mode=OneWay}"/>  <!--works-->

Page1 code-behind (C#)

[...]
        public MyClass Test { get; }
[...]

Page2 code-behind (C#)

[...]
            Page1 P1 = new();
            TextBox.Text = P1.Test.TestSwitch.ToString();
[...]

4. Find more examples including MVVM in this test app.


This is a general question about how to do this the right way. Did anyone share data in this context before?

*** Please excuse my amateur language.


Solution

  • IMHO your MVVM approach seems wrong, at least if I fully understood your requirements.

    In your provided demo application you have 2 classes TestVM and TestVM2, but you have 3 independent working properties of TestSwitch.

    • TestVM has a member TestSwitch, so each instance of TestVM will have its own state
    • TestVM2 has a member TestSwitch, so each instance of TestVM2 will have its own state
    • TestVM2 has a static instance, with its own state

    I would suggest offloading all "shared" members into a seperate class eg. TestVMShared and use a static instance of this class.

    So instead of this (code is from the github repository)

    public class TestVM : INotifyPropertyChanged
    {
        private bool testSwitch;
    
        public bool TestSwitch
        {
            get => testSwitch;
            set
            {
                if (testSwitch != value)
                {
                    testSwitch = value;
                    OnPropertyChanged(nameof(TestSwitch));
                }
            }
        }
    
        public event PropertyChangedEventHandler? PropertyChanged;
    
        private void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    public class TestVM2 : INotifyPropertyChanged
    {
        //{Binding Source={x:Static local:TestVM.Instance}, Path=TestSwitch}
        private bool testSwitch;
        
        public TestVM2() {}
    
        public bool TestSwitch
        {
            get => testSwitch;
            set
            {
                if (testSwitch != value)
                {
                    testSwitch = value;
                    OnPropertyChanged(nameof(TestSwitch));
                }
            }
        }
    
        private static readonly TestVM2? _instance;
    
        public static TestVM2 Instance => _instance ?? new TestVM2();
    
        public event PropertyChangedEventHandler? PropertyChanged;
    
        private void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    

    I would do this:

    // note: 
    // I added this class, but it is irrelevant to the answer it just reduces code
    // you should consider looking into using a library offering MVVM goodies 
    // eg. https://github.com/CommunityToolkit/WindowsCommunityToolkit
    
    public class VMBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
        
        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    
    public class TestVM : VMBase
    {
        public TestVMShared Shared { get; } = TestVMShared.Instance;
    }
    public class TestVM2 : VMBase
    {
        public TestVMShared Shared { get; } = TestVMShared.Instance;
    }
    
    public class TestVMShared : VMBase
    {
        public static TestVMShared Instance { get; } = new();
    
        private bool testSwitch;
    
        public bool TestSwitch
        {
            get => testSwitch;
            set
            {
                if (testSwitch != value)
                {
                    testSwitch = value;
                    OnPropertyChanged(nameof(TestSwitch));
                }
            }
        }
    }
    

    With this every instance either of TestVM or TestVM2 will always use the same instance of TestVMShared, as I am using the static instance.

    Edit:

    Didn't quite understand, why you only specified an DesignInstance in Tab_One.xaml, so I did the following additions

    enter image description here