leocb / MaterialSkin

Theming .NET WinForms, C# or VB.Net, to Google's Material Design Principles.
MIT License
437 stars 132 forks source link

Cross-thread exception in TabControl when setting MaterialSkin.ColorScheme #325

Open ainwood opened 2 years ago

ainwood commented 2 years ago

I wanted the users to be able to change the color scheme. I have a watcher that monitors the settings in a database, and if they change the values, the watcher kicks-off a background worker that applies the new theme.

This worked great - changed the MaterialForm colors, MaterialCard colors and MaterialButton colors. But I get an InvalidOperationException with the MaterialTabControl.

System.InvalidOperationException
  HResult=0x80131509
  Message=Cross-thread operation not valid: Control 'tabPage2' accessed from a thread other than the thread it was created on.
  Source=System.Windows.Forms
  StackTrace:
   at System.Windows.Forms.Control.get_Handle()
   at System.Windows.Forms.Control.Invalidate(Boolean invalidateChildren)
   at System.Windows.Forms.TabPage.set_BackColor(Color value)
   at MaterialSkin.MaterialSkinManager.UpdateControlBackColor(Control controlToUpdate, Color newBackColor)
   at MaterialSkin.MaterialSkinManager.UpdateControlBackColor(Control controlToUpdate, Color newBackColor)
   at MaterialSkin.MaterialSkinManager.UpdateControlBackColor(Control controlToUpdate, Color newBackColor)
   at MaterialSkin.MaterialSkinManager.UpdateControlBackColor(Control controlToUpdate, Color newBackColor)
   at MaterialSkin.MaterialSkinManager.UpdateBackgrounds()
   at MaterialSkin.MaterialSkinManager.set_ColorScheme(ColorScheme value)
   at eLog.Elements.Theme.ThemeChangeWorker_DoWork(Object sender, DoWorkEventArgs e) in C:\Users\Theme.cs:line 139

  This exception was originally thrown at this call stack:
    [External Code]
    Theme.ThemeChangeWorker_DoWork(object, System.ComponentModel.DoWorkEventArgs) in Theme.cs

Note: What is strange is that: When the main form is initially created, the SkinManager is set to a default blue theme. SkinManagerInstance = MaterialSkinManager.Instance; SkinManagerInstance.AddFormToManage(this); SkinManagerInstance.Theme = MaterialSkinManager.Themes.LIGHT; SkinManagerInstance.ColorScheme = new ColorScheme(Primary.Blue800, Primary.Blue900, Primary.Blue500, Accent.LightBlue200, TextShade.WHITE);

I then create my application model, which sets-up the watcher. This retrieves the initial theme values from the database, and kicks-off the worker that changes the theme. This first change appears to work OK.

It is only subsequent to this that a change in the values causes the cross-thread issue.

'eTestApp.exe' (CLR v4.0.30319: eTestApp.exe): Loaded 'C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.DirectoryServices.Protocols\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.DirectoryServices.Protocols.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled. New theme detected. //<=Watcher loading the theme for the first time. MaterialSkin.ColorScheme //<=Theme applied, no problem. The thread 0x1a08 has exited with code 0 (0x0). New theme detected. //<=Value changed in database, detected by Watcher. Exception thrown: 'System.InvalidOperationException' in System.Windows.Forms.dll //<=Exception occurs.

Ultimately, I can work-around this by making the changes to the themes when the application isn't running. I just wanted to let people tweak values and see the changes in real-time.

ainwood commented 2 years ago

This is how I solved it.

        private delegate void safeSetSkinColourDelegate(Control control, MaterialSkinManager manager, ColorScheme newScheme);
        public static void SafeSetSkinColour(this Control control, MaterialSkinManager manager, ColorScheme newScheme)
        {
            if (control.InvokeRequired)
            {
                safeSetSkinColourDelegate _delegate = new safeSetSkinColourDelegate(SafeSetSkinColour);
                control.Invoke(_delegate, new object[] { control, manager, newScheme });
            }
            else
            {
                manager.ColorScheme = newScheme;
                control.Refresh();
            }
        }

Called by: MainForm.SafeSetSkinColour(ManagerInstance, new MaterialSkin.ColorScheme(Primary, DarkPrimary, LightPrimary, Accent, TextShade));

Edit - based on this, the behaviour is arguably not a bug. But inconsistent behaviour amongst controls.