diff --git a/dotnet-desktop-guide/net/winforms/toc.yml b/dotnet-desktop-guide/net/winforms/toc.yml index ce41f7c774..28ebda2d2c 100644 --- a/dotnet-desktop-guide/net/winforms/toc.yml +++ b/dotnet-desktop-guide/net/winforms/toc.yml @@ -3,6 +3,8 @@ items: href: index.yml - name: What's new items: + - name: What's new in .NET 8.0 + href: whats-new/net80.md - name: What's new in .NET 7.0 href: whats-new/net70.md - name: What's new in .NET 6.0 diff --git a/dotnet-desktop-guide/net/winforms/whats-new/media/net80/commands.png b/dotnet-desktop-guide/net/winforms/whats-new/media/net80/commands.png new file mode 100644 index 0000000000..c9a6e94ff8 Binary files /dev/null and b/dotnet-desktop-guide/net/winforms/whats-new/media/net80/commands.png differ diff --git a/dotnet-desktop-guide/net/winforms/whats-new/net70.md b/dotnet-desktop-guide/net/winforms/whats-new/net70.md index a7ffbed823..0943d86af0 100644 --- a/dotnet-desktop-guide/net/winforms/whats-new/net70.md +++ b/dotnet-desktop-guide/net/winforms/whats-new/net70.md @@ -63,7 +63,7 @@ This release adds further improvements to accessibility, including but not limit While Windows Forms already had a powerful binding engine, a more modern form of data binding, similar to data binding provided by WPF, is being introduced. -The new data binding features allow you to fully embrace the MVVM pattern and the use of object-relational mappers from ViewModels in Windows Forms easier than before. This, in turn, makes it possible to reduce code in the code-behind files, and opens new testing possibilities. More importantly, it enables code sharing between Windows Forms and other .NET GUI frameworks such as WPF, UWP/WinUI, and MAUI. And to clarify a commonly asked question, there aren't any plans to introduce XAML in Windows Forms. +The new data binding features allow you to fully embrace the MVVM pattern and the use of object-relational mappers from ViewModels in Windows Forms easier than before. This, in turn, makes it possible to reduce code in the code-behind files, and opens new testing possibilities. More importantly, it enables code sharing between Windows Forms and other .NET GUI frameworks such as WPF, UWP/WinUI, and .NET MAUI. And to clarify a commonly asked question, there aren't any plans to introduce XAML in Windows Forms. These new data binding features are in preview for .NET 7, and more work on this feature will happen in .NET 8. diff --git a/dotnet-desktop-guide/net/winforms/whats-new/net80.md b/dotnet-desktop-guide/net/winforms/whats-new/net80.md new file mode 100644 index 0000000000..85ebe62e8a --- /dev/null +++ b/dotnet-desktop-guide/net/winforms/whats-new/net80.md @@ -0,0 +1,78 @@ +--- +title: What's new in Windows Forms .NET 8 +description: Learn about what's new in Windows Forms for .NET 8. Windows Forms. .NET provides new features and enhancements over .NET 7. +ms.date: 11/29/2023 +ms.topic: conceptual +--- + +# What's new for .NET 8 (Windows Forms .NET) + +This article describes some of the new Windows Forms features and enhancements in .NET 8. + +There are a few breaking changes you should be aware of when migrating from .NET Framework to .NET 8. For more information, see [Breaking changes in Windows Forms](/dotnet/core/compatibility/winforms). + +## Data binding improvements + +A new data binding engine was in preview with .NET 7, and is now fully enabled in .NET 8. Though not as extensive as the existing Windows Forms data binding engine, this new engine is modeled after WPF, which makes it easier to implement MVVM design principles. + +The enhanced data binding capabilities make it simpler to fully utilize the MVVM pattern and employ object-relational mappers from ViewModels in Windows Forms. This reduces the amount of code in code-behind files. More importantly, it enables code sharing between Windows Forms and other .NET GUI frameworks like WPF, UWP/WinUI, and .NET MAUI. It's important to note that while the previously mentioned GUI frameworks use XAML as a UI technology, XAML isn't coming to Windows Forms. + +The interface and the class drive the new binding system. implements the interface and provides new data binding capabilities to Windows Forms. + +## Button commands + +Button commands were in preview with .NET 7, and is now fully enabled in .NET 8. Similar to WPF, the instance of an object that implements the interface can be assigned to the button's property. When the button is clicked, the command is invoked. + +An optional parameter can be provided when the command is invoked, by the specifying a value for the button's property. + +The `Command` and `CommandParameter` properties are set in the designer through the **Properties** window, under **(DataBindings)**, as illustrated by the following image. + +:::image type="content" source="media/net80/commands.png" alt-text="The Visual Studio properties window highlighting a Windows Forms' Button's Command and CommandParameter properties."::: + +Buttons also listen to the event, which causes the control to query the method. When that method returns `true`, the control is enabled; the control is disabled when `false`is returned. + +## Visual Studio DPI improvements + +Visual Studio 2022 17.8 Introduces DPI-unwaware designer tabs. Previously, the Windows Designer tab in Visual Studio ran at the DPI of Visual Studio. This causes problems when you're designing a DPI-unaware Windows Forms app. Now you can ensure that the designer runs at the same scale as you want the app to run, either DPI-aware or not. Before this feature was introduced, you had to run Visual Studio in DPI-unaware mode, which made Visual Studio itself blurry when scaling was applied in Windows. Now you can leave Visual Studio alone and let the designer run DPI-unaware. + +You can enable the DPI-unaware designer for the Windows Forms project by adding `` to the project file, and setting the value to `true`. + +:::code language="xml" source="./snippets/net80/csharp/snippets.csproj" range="3-11" highlight="7"::: + +> [!IMPORTANT] +> Visual Studio reads this setting when the project is loaded, and not when it's changed. After changing this setting, unload and reload your project to get Visual Studio to respect it. + +## High DPI improvements + +High DPI rendering with has been improved: + +- Correctly scale nested controls. For example, a button that's in a panel, which is placed on a tab page. +- Scale and properties based on the current monitor DPI settings. + + Starting with .NET 8, this feature is enabled by default and you need to opt out of it to revert to the previous behavior. + + To disable the feature, add `System.Windows.Forms.ScaleTopLevelFormMinMaxSizeForDpi` to the `configProperties` setting in [_runtimeconfig.json_](/dotnet/core/runtime-config/#runtimeconfigjson), and set the value to false: + + ```json + { + "runtimeOptions": { + "tfm": "net8.0", + "frameworks": [ + ... + ], + "configProperties": { + "System.Windows.Forms.ScaleTopLevelFormMinMaxSizeForDpi": false, + } + } + } + ``` + +## Miscellaneous improvements + +Here are some other notable changes: + +- The code that handled `FolderBrowserDialog` was improved, fixing a few memory leaks. +- The code base for Windows Forms has been slowly enabling C# nullability, rooting out any potential null-reference errors. +- The `System.Drawing` source code was migrated to the [Windows Forms GitHub repository](https://github.com/dotnet/winforms). +- Modern Windows icons can be accessed by a new API, . The enumeration lists all of the available system icons. +- More designers are available at run-time now. For more information, see [GitHub issue #4908](https://github.com/dotnet/winforms/issues/4908). diff --git a/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Company.cs b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Company.cs new file mode 100644 index 0000000000..3d7c337e6b --- /dev/null +++ b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Company.cs @@ -0,0 +1,43 @@ +using System.Windows.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace snippets; + +public class Company: ObservableObject, IDisposable +{ + int _employeeCount = 10; + //Bitmap _icon = SystemIcons.GetStockIcon(StockIconId.Warning, StockIconOptions.Default).ToBitmap(); + Bitmap _icon = SystemIcons.Warning.ToBitmap(); + + public Bitmap CompanyImage => _icon; + + public int EmployeeCount + { + get => _employeeCount; + set => SetProperty(ref _employeeCount, value); + } + + public ICommand IncreaseEmployeeCommand { get; } + + ~Company() + { + Dispose(); + } + + public Company() + { + IncreaseEmployeeCommand = new RelayCommand(IncreaseEmployeeCount, (i) => EmployeeCount < 20); + } + + private void IncreaseEmployeeCount(int count) + { + EmployeeCount += count; + ((RelayCommand)IncreaseEmployeeCommand).NotifyCanExecuteChanged(); + } + + public void Dispose() + { + _icon.Dispose(); + } +} diff --git a/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/DataBindingExample.Designer.cs b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/DataBindingExample.Designer.cs new file mode 100644 index 0000000000..70c5424c9a --- /dev/null +++ b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/DataBindingExample.Designer.cs @@ -0,0 +1,135 @@ +namespace snippets +{ + partial class DataBindingExample + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + button1 = new Button(); + companyBindingSource = new BindingSource(components); + label1 = new Label(); + label2 = new Label(); + btnCompany1 = new Button(); + btnCompany2 = new Button(); + pictureBox1 = new PictureBox(); + ((System.ComponentModel.ISupportInitialize)companyBindingSource).BeginInit(); + ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); + SuspendLayout(); + // + // button1 + // + button1.DataBindings.Add(new Binding("Command", companyBindingSource, "IncreaseEmployeeCommand", true)); + button1.Location = new Point(12, 12); + button1.Name = "button1"; + button1.Size = new Size(170, 23); + button1.TabIndex = 0; + button1.Text = "Increase Employee Count"; + button1.UseVisualStyleBackColor = true; + // + // companyBindingSource + // + companyBindingSource.DataSource = typeof(Company); + // + // label1 + // + label1.AutoSize = true; + label1.Location = new Point(12, 45); + label1.Name = "label1"; + label1.Size = new Size(98, 15); + label1.TabIndex = 1; + label1.Text = "Employee Count:"; + // + // label2 + // + label2.AutoSize = true; + label2.DataBindings.Add(new Binding("Text", companyBindingSource, "EmployeeCount", true)); + label2.Location = new Point(116, 45); + label2.Name = "label2"; + label2.Size = new Size(13, 15); + label2.TabIndex = 2; + label2.Text = "0"; + // + // btnCompany1 + // + btnCompany1.Location = new Point(12, 123); + btnCompany1.Name = "btnCompany1"; + btnCompany1.Size = new Size(207, 23); + btnCompany1.TabIndex = 3; + btnCompany1.Text = "Load Company 1 (10 employess)"; + btnCompany1.UseVisualStyleBackColor = true; + btnCompany1.Click += btnCompany1_Click; + // + // btnCompany2 + // + btnCompany2.Location = new Point(12, 152); + btnCompany2.Name = "btnCompany2"; + btnCompany2.Size = new Size(207, 23); + btnCompany2.TabIndex = 4; + btnCompany2.Text = "Load Company 2 (3 employess)"; + btnCompany2.UseVisualStyleBackColor = true; + btnCompany2.Click += btnCompany2_Click; + // + // pictureBox1 + // + pictureBox1.DataBindings.Add(new Binding("Image", companyBindingSource, "CompanyImage", true)); + pictureBox1.Location = new Point(12, 67); + pictureBox1.Name = "pictureBox1"; + pictureBox1.Size = new Size(100, 50); + pictureBox1.TabIndex = 5; + pictureBox1.TabStop = false; + // + // DataBindingExample + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(238, 192); + Controls.Add(pictureBox1); + Controls.Add(btnCompany2); + Controls.Add(btnCompany1); + Controls.Add(label2); + Controls.Add(label1); + Controls.Add(button1); + Name = "DataBindingExample"; + Text = "DataBindingExample"; + DataContextChanged += DataBindingExample_DataContextChanged; + ((System.ComponentModel.ISupportInitialize)companyBindingSource).EndInit(); + ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Button button1; + private Label label1; + private Label label2; + private BindingSource companyBindingSource; + private Button btnCompany1; + private Button btnCompany2; + private PictureBox pictureBox1; + } +} diff --git a/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/DataBindingExample.cs b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/DataBindingExample.cs new file mode 100644 index 0000000000..5dfd313fb0 --- /dev/null +++ b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/DataBindingExample.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace snippets +{ + public partial class DataBindingExample : Form + { + public DataBindingExample() + { + InitializeComponent(); + button1.CommandParameter = 5; + } + + private void DataBindingExample_DataContextChanged(object sender, EventArgs e) => + companyBindingSource.DataSource = DataContext; + + private void btnCompany1_Click(object sender, EventArgs e) + { + DataContext = new Company() { EmployeeCount = 10 }; + + } + + private void btnCompany2_Click(object sender, EventArgs e) + { + DataContext = new Company() { EmployeeCount = 3 }; + } + } +} diff --git a/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/DataBindingExample.resx b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/DataBindingExample.resx new file mode 100644 index 0000000000..6915fcb700 --- /dev/null +++ b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/DataBindingExample.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Form1.Designer.cs b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Form1.Designer.cs new file mode 100644 index 0000000000..07a786dfd0 --- /dev/null +++ b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Form1.Designer.cs @@ -0,0 +1,144 @@ +namespace snippets; + +partial class Form1 +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + button1 = new Button(); + checkBox1 = new CheckBox(); + dateTimePicker1 = new DateTimePicker(); + richTextBox1 = new RichTextBox(); + label1 = new Label(); + label2 = new Label(); + label3 = new Label(); + label4 = new Label(); + SuspendLayout(); + // + // button1 + // + button1.Location = new Point(12, 12); + button1.Name = "button1"; + button1.Size = new Size(75, 23); + button1.TabIndex = 0; + button1.Text = "button1"; + button1.UseVisualStyleBackColor = true; + button1.Click += button1_Click; + // + // checkBox1 + // + checkBox1.AutoSize = true; + checkBox1.Location = new Point(12, 41); + checkBox1.Name = "checkBox1"; + checkBox1.Size = new Size(83, 19); + checkBox1.TabIndex = 1; + checkBox1.Text = "checkBox1"; + checkBox1.UseVisualStyleBackColor = true; + // + // dateTimePicker1 + // + dateTimePicker1.Location = new Point(12, 66); + dateTimePicker1.Name = "dateTimePicker1"; + dateTimePicker1.ShowCheckBox = true; + dateTimePicker1.Size = new Size(208, 23); + dateTimePicker1.TabIndex = 2; + // + // richTextBox1 + // + richTextBox1.Location = new Point(226, 34); + richTextBox1.Name = "richTextBox1"; + richTextBox1.Size = new Size(167, 55); + richTextBox1.TabIndex = 3; + richTextBox1.Text = ""; + // + // label1 + // + label1.AutoSize = true; + label1.Location = new Point(226, 16); + label1.Name = "label1"; + label1.Size = new Size(106, 15); + label1.TabIndex = 4; + label1.Text = "RichTextBox below"; + // + // label2 + // + label2.AutoSize = true; + label2.Font = new Font("Segoe UI Variable Display", 9F, FontStyle.Regular, GraphicsUnit.Point); + label2.Location = new Point(12, 92); + label2.Name = "label2"; + label2.Size = new Size(75, 16); + label2.TabIndex = 5; + label2.Text = "Font preview"; + // + // label3 + // + label3.AutoSize = true; + label3.Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point); + label3.Location = new Point(12, 107); + label3.Name = "label3"; + label3.Size = new Size(75, 15); + label3.TabIndex = 6; + label3.Text = "Font preview"; + // + // label4 + // + label4.AutoSize = true; + label4.Location = new Point(12, 122); + label4.Name = "label4"; + label4.Size = new Size(75, 15); + label4.TabIndex = 7; + label4.Text = "Font preview"; + // + // Form1 + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(403, 161); + Controls.Add(label4); + Controls.Add(label3); + Controls.Add(label2); + Controls.Add(label1); + Controls.Add(richTextBox1); + Controls.Add(dateTimePicker1); + Controls.Add(checkBox1); + Controls.Add(button1); + Name = "Form1"; + Text = "Form1"; + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Button button1; + private CheckBox checkBox1; + private DateTimePicker dateTimePicker1; + private RichTextBox richTextBox1; + private Label label1; + private Label label2; + private Label label3; + private Label label4; +} diff --git a/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Form1.cs b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Form1.cs new file mode 100644 index 0000000000..1c93ab9832 --- /dev/null +++ b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Form1.cs @@ -0,0 +1,16 @@ +namespace snippets; + +public partial class Form1 : Form +{ + public Form1() + { + InitializeComponent(); + } + + private void button1_Click(object sender, EventArgs e) + { + FolderBrowserDialog dialog = new FolderBrowserDialog(); + dialog.ShowDialog(); + + } +} diff --git a/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Form1.resx b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Form1.resx new file mode 100644 index 0000000000..af32865ec1 --- /dev/null +++ b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Form1.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Program.cs b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Program.cs new file mode 100644 index 0000000000..1803f47b28 --- /dev/null +++ b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Program.cs @@ -0,0 +1,16 @@ +namespace snippets; + +static class Program +{ + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new DataBindingExample()); + } +} diff --git a/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Properties/DataSources/Company.datasource b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Properties/DataSources/Company.datasource new file mode 100644 index 0000000000..826f98118a --- /dev/null +++ b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/Properties/DataSources/Company.datasource @@ -0,0 +1,10 @@ + + + + snippets.Company, snippets, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/snippets.csproj b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/snippets.csproj new file mode 100644 index 0000000000..51aab9f023 --- /dev/null +++ b/dotnet-desktop-guide/net/winforms/whats-new/snippets/net80/csharp/snippets.csproj @@ -0,0 +1,18 @@ + + + + WinExe + net8.0-windows + enable + true + enable + false + DpiUnawareGdiScaled + + + + + + + + \ No newline at end of file