Serenity 6.9.6 Release Notes (2023-11-26)

New Form / Column Property Name Analyzers and Code Fixes

In Serenity, form and column definitions generally look similar to the following:

[FormScript("Administration.User")]
[BasedOnRow(typeof(UserRow), CheckNames = true)]
public class UserForm
{
    [LabelWidth(200, UntilNext = true)]
    public string Username { get; set; }
    public string DisplayName { get; set; }

The BasedOnRow attribute determines that this grid column definition is based on the UserRow, meaning the properties in the columns definition inherit the corresponding properties with the same name.

The assignment CheckNames = true in the BasedOnRow attribute ensures that any property defined in the columns class should have a matching property in the row class with the same name and casing.

If CheckNames is set to true and you want to add an extra property that does not exist in the row class, you should add the [IgnoreName] attribute on top of it. This ensures that the property naming was intentional and not a typo.

[FormScript("Administration.User")]
[BasedOnRow(typeof(UserRow), CheckNames = true)]
public class UserForm
{
    [LabelWidth(200, UntilNext = true)]
    public string Username { get; set; }
    public string DisplayName { get; set; }

    [IgnoreName]
    public string SomeExtraProperty { get; set; }
    //...
}

Failure to add an [IgnoreName] attribute will result in Serenity raising a runtime error:

System.InvalidProgramException: StartSharp.Administration.UserForm 
has a [BasedOnRow(typeof(StartSharp.Administration.UserRow), CheckNames = true)] 
attribute, but its 'SomeExtraProperty' property doesn't have a matching field 
with the same property/field name in the row. Please check if the property is named correctly.

To remove this validation, you may set CheckNames to false in the [BasedOnRow] attribute.
To disable checking for this specific property, add an [IgnoreName] attribute to the property itself.

This name-checking feature is very useful as it helps avoid typos when entering column and form properties. Sometimes these mistakes are related to casing, such as using Displayname instead of DisplayName, or forgetting to remove/rename some property from columns or forms when removing/renaming a property in the row class. Such errors generally lead to JSON serialization errors or empty column/form input problems.

Since the validation is performed at runtime, you may not be aware of the typo until you open a grid or display a form when the error is shown. This can be problematic if you deployed an application without knowing that one of the screens is going to fail.

To overcome this issue and improve the developer experience, we have prepared an analyzer for checking property names while you are coding in the IDE:

SomeExtraProperty Error

As seen in the screenshot above, the analyzer in Serenity.Pro.Coder checks the property name during development and reports an error with code SRN001 ('SomeExtraProperty' does not match any property name in the UserRow...).

It also offers a code fix with the option to 'Add [IgnoreName] attribute,' which you can access by clicking the light bulb icon (or Ctrl+.). Clicking this option will automatically add the [IgnoreName] attribute to the property itself.

As we mentioned, one of the most common mistakes is related to casing, such as typing Displayname instead of DisplayName. Since C# and JavaScript are case-sensitive, this can be a problem. In this case, our analyzer can identify the issue and offer a better solution:

Rename to DisplayName

This time, the analyzer detects that this is a casing error and offers to rename the property to DisplayName to match the UserRow class.

Note that the first option in the menu, e.g., 'Rename DisplayName to Displayname,' is not produced by our analyzer; it is generated by Visual Studio itself, probably via some spell-check system, and does nothing when you click it.

All these errors are also produced during the build, so even if you don't have a particular form/columns file with an issue open in the editor, you'll still get an error during the build. This way, having the error at runtime can be avoided.

New Row Property Getter/Setter Analyzer and Code Fixes

Serenity entities have properties with get/set methods like the following:

public string Username { get => fields.Username[this]; set => fields.Username[this] = value; }

These get/set methods are required to be written in this way for the Serenity entity system to work properly and facilitate features like assignment tracking and change notifications.

The fields used within these statements are usually defined in a RowFields nested class:

public StringField Username;

Each property should use a row field that matches its name. You normally have to define these fields manually and write the get/set methods that reference the correct field.

We recently introduced a [GenerateFields] attribute that allows the Serenity.Pro.Coder source generator to create these fields for you. However, you still have to write the get/set methods yourself as C# does not yet support partial properties.

Whether you use GenerateFields or manually define fields, you still have to write and ensure the correct get/set statements for all your row properties. Sometimes, it is possible to reference a different field by mistake while writing a get/set method:

public string Username { get => fields.Username[this]; set => fields.DisplayName[this] = value; }

Here, the get method uses the correct field, but the set method is setting the display name. You might think this should be very rare, but we have observed many developers making such mistakes, especially when adding new row properties manually. This is a serious mistake and can lead to many problems at runtime, such as overwriting the wrong property or producing unexpected results during JSON serialization/deserialization.

Until C# introduces partial properties, which does not seem likely to happen soon, we are now introducing a new analyzer for row property get/set methods in Serenity.Pro.Coder:

Property Getter Setter Error

It detects that the Username property is referencing the wrong field in its setter and informs us accordingly.

Another useful feature of this analyzer is its ability to generate getter/setter code for you if you simply write a basic { get; set; } statement:

Generate Getter/Setter with Auto Property

It works even if you just open and close braces, e.g., {}:

Generate Getter/Setter with Open/Close Braces

This way, you can avoid manually writing getter/setter code for row properties and let Pro.Coder handle that for you.

When defining a new row type, it is also possible to quickly generate multiple get/set statements in a class by using the "Fix all occurrences in document" option:

Quickly Generate Multiple Getters/Setters

Combined with the GenerateFields attribute, we believe this will make it much easier to define new entities or add properties to existing ones, even without partial properties in C#.

Please note that the analyzer currently inspects all public properties of a row class and assumes they should have proper get/set methods accessing their corresponding fields unless they are getter-only. In most cases, this should not cause any issues, but if you have any extra properties in your row class that don't have corresponding row fields for some reason, you will need to use the "Suppress or configure issues" option in the dropdown to turn this analyzer off per property or for the entire file by adding a pragma statement:

[GenerateFields]
public sealed partial class ClientRow
{
    public string ClientId { get => fields.ClientId[this]; set => fields.ClientId[this] = value; }
    public string ClientName { get => fields.ClientName[this]; set => fields.ClientName[this] = value; }
    public string City { get => fields.City[this]; set => fields.City[this] = value; }
#pragma warning disable SRN0005
    public string SomeIrregularProperty { get; set; }
#pragma warning restore SRN0005
}

Changed Default Ignore Option for jQuery Validation

Previously, we utilized the :hidden selector to filter out elements that should not undergo validation within a form. This selector, specific to jQuery (unavailable in CSS), targets elements not displayed, such as those with inline styles like style="display: none", or those having classes like .hidden or .d-none, effectively rendering them as display: none. It also includes elements within a parent element set to display: none. Furthermore, the :hidden selector encompasses elements with zero width or height.

Filtering out these elements makes sense in scenarios where an editor possessing the required class resides within a hidden .field div, preventing the need for validation. For instance, properties hidden from the form using methods like this.form.SomeEditor.getGridField().hide() cannot be edited by the user and hence need not be required.

It's important to note that the :hidden selector excludes elements with the CSS property visibility: hidden if they possess non-zero dimensions, ensuring their validation despite being hidden from the end user.

However, using this selector presented an issue beyond its reliance on jQuery. It also included elements under an inactive tab, meaning fields not currently active wouldn't undergo validation. To resolve this, we applied a workaround to inactive tabs by overriding the CSS for jQuery UI Tabs and Bootstrap Tabs, setting them to visibility: hidden. This ensured that fields under inactive tabs were still validated when the save button was pressed.

While this approach functioned acceptably, occasional issues arose due to conflicts with tab-related CSS properties and display properties. For example, when a new Bootstrap version altered the CSS selector for showing/hiding tabs or set them to display: flex, our existing rules would sometimes override theirs, or vice versa. This led to problems such as the active tab becoming hidden or inactive tabs becoming visible. Furthermore, as elements in hidden tabs effectively had dimensions, components like SlickGrid or ChartJS considered these elements visible, resulting in erroneous calculations or initialization. Overcoming such issues required several additional workarounds.

In this version, we've chosen to cease battling these inherent challenges and have modified the default selector to [style*="display:none"], [style*="display: none"] *, .hidden *, input[type=hidden"]. This selector operates solely in CSS and identifies elements that are display: none, have parent elements set to display: none, are within parent elements with the hidden class overriding their display property to none, or represent hidden type inputs. Additionally, we removed our overrides of tab-related CSS styles.

Ensure that versions of Serenity.Assets, Serenity.Scripts, and Serenity.Pro.Theme packages match, as this change necessitates all of them to be at 6.9.6+ for proper functionality. Serene users should also update their serenity.css and common-theme.css files after upgrading to version 6.9.6.

Experimental SQL Upload Storage Implementation

We've introduced an experimental IDiskUploadFileSystem implementation within the Serenity.Pro.Extensions package. This implementation allows files and directories to be stored in a database table named FileSystem. Additionally, we've provided a SqlUploadStorage implementation that utilizes the aforementioned SqlFileSystem for file uploads.

For StartSharp users, registering this file system in Startup.cs is possible by implementing the following:

services.AddSingleton<IUploadStorage, SqlUploadStorage>();
//...
// Ensure the above line precedes the AddUploadStorage line below
services.AddUploadStorage();

Please note, this feature is experimental and recommended only for development environments.