WPF Open Source Project: WPF-ControlBase

WPF Open Source Project: WPF-ControlBase

The repository README is plain, but after reading a few blog posts linked in the author's README, you will like it.

Last updated 11/30/2021 4:57 PM
He BianGu
34 min read
Category
WPF
Topic
WPF Control Library WPF Open Source Projects
Tags
.NET WPF WPF Open Source Projects Open Source Projects Open Source

仓库截图

The repository README is very plain, but after reading the author's blog posts linked in the README, you will like it. Without further ado, here is the table of contents:

  1. Animation Encapsulation
https://blog.csdn.net/u010975589/article/details/95974854
  1. Property Form
https://blog.csdn.net/u010975589/article/details/95970200
  1. Message Dialog
https://blog.csdn.net/u010975589/article/details/95985190
  1. Applying MVC in WPF
https://blog.csdn.net/u010975589/article/details/100019431
  1. Other Feature Descriptions
https://blog.csdn.net/u010975589/article/details/103083605

Detailed introduction below:

1. Animation Encapsulation

Original title: Example: Custom StoryBoardService in WPF to encapsulate StoryBoard and Animation in code to simplify animation writing
Original link: https://blog.csdn.net/u010975589/article/details/95974854

1.1 Purpose: Simplify animation writing by encapsulating StoryBoard and Animation

1.2 Example

Description: Fade out is a commonly used animation in WPF. The image above shows the effect after encapsulation via StoryBoarService. In code, just execute the following:

DoubleStoryboardEngine.Create(1, 0, 1, "Opacity").Start(element);

The close effect above can be defined as a command like this:

public class CollapsedOfOpacityCommand : ICommand
{

    public bool CanExecute(object parameter) => true;

    public void Execute(object parameter)
    {
        if(parameter is UIElement element)
        {
            var engine = DoubleStoryboardEngine.Create(1, 0, 1, "Opacity");

            engine.Start(element);
        }
    }

    public event EventHandler CanExecuteChanged;
}

Invoke the following command in Xaml to achieve the close fade-out effect:

Command="{x:Static base:CommandService.CollapsedOfOpacityCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource
AncestorType=GroupBox}}"

The CommandParameter passed in will fade out when the command is executed.

The animation effect only requires one line of code, simplifying the tedious coding process for animations in code.

DoubleStoryboardEngine.Create(1, 0, 1, "Opacity").Start(element);

1.3 Code:

Currently, only the DoubleAnimation encapsulation is implemented; other types will be encapsulated later.

1.3.1 Base class for encapsulation modification

/// <summary> Animation engine base class </summary>
public abstract class StoryboardEngineBase : IDisposable
{
    protected Storyboard storyboard = new Storyboard();

    public EventHandler CompletedEvent { get; set; }

    public EasingFunctionBase Easing { get; set; } = EasingFunctionFactroy.PowerEase;

    public PropertyPath PropertyPath { get; set; }

    public Duration Duration { get; set; }

    public void Dispose()
    {
        storyboard.Completed -= CompletedEvent;
    }

    public abstract StoryboardEngineBase Start(UIElement element);

    public abstract StoryboardEngineBase Stop();

    public StoryboardEngineBase(int second, string property)
    {
        this.PropertyPath = new PropertyPath(property);
        this.Duration = new Duration(TimeSpan.FromSeconds(second));
    }

}

/// <summary> Animation generic engine base class </summary>
public abstract class StoryboardEngineBase<T> : StoryboardEngineBase
{
    public StoryboardEngineBase(T from, T to, int second, string property) : base(second, property)
    {
        this.FromValue = from;
        this.ToValue = to;
    }

    public T FromValue { get; set; }

    public T ToValue { get; set; }

    //public RepeatBehavior RepeatBehavior { get; set; };

}

1.3.2 Exposed extension DoubleStoryboardEngine

/// <summary> DoubleAnimation engine </summary>
public class DoubleStoryboardEngine : StoryboardEngineBase<double>
{
    public static DoubleStoryboardEngine Create(double from, double to, int second, string property)
    {
        return new DoubleStoryboardEngine(from, to, second, property);
    }

    public DoubleStoryboardEngine(double from, double to, int second, string property) : base(from, to, second, property)
    {

    }

    public override StoryboardEngineBase Start(UIElement element)
    {
        //  Do: Timeline
        DoubleAnimation animation = new DoubleAnimation(1, 0, this.Duration);

        if (this.Easing != null)
            animation.EasingFunction = this.Easing;

        //if (this.RepeatBehavior != default(RepeatBehavior))
        //    animation.RepeatBehavior = (RepeatBehavior);

        //  Do: Property animation
        storyboard.Children.Add(animation);
        Storyboard.SetTarget(animation, element);
        Storyboard.SetTargetProperty(animation, this.PropertyPath);

        if (CompletedEvent != null)
            storyboard.Completed += CompletedEvent;
        storyboard.Begin();

        return this;
    }

    public override StoryboardEngineBase Stop()
    {
        this.storyboard.Stop();

        return this;
    }
}

1.3.3 Easing function factory

/// <summary> Description: https://docs.microsoft.com/en-us/dotnet/framework/wpf/graphics-multimedia/easing-functions </summary>
public static class EasingFunctionFactroy
{
    /// <summary> PowerEase: Creates an animation that accelerates and/or decelerates using the formula f(t) = t^p, where p equals the Power property. </summary>
    public static PowerEase PowerEase { get; set; } = new PowerEase();
    /// <summary> BackEase: Retracts the motion of an animation slightly before it begins to animate in the path indicated. </summary>
    public static BackEase BackEase { get; set; } = new BackEase();
    /// <summary> ElasticEase: Creates an animation that resembles a spring oscillating back and forth until it comes to rest. </summary>
    public static ElasticEase ElasticEase { get; set; } = new ElasticEase();
    /// <summary> BounceEase: Creates a bouncing effect. </summary>
    public static BounceEase BounceEase { get; set; } = new BounceEase();
    /// <summary> CircleEase: Creates an animation that accelerates and/or decelerates using a circular function. </summary>
    public static CircleEase CircleEase { get; set; } = new CircleEase();

    /// <summary> QuadraticEase: Creates an animation that accelerates and/or decelerates using the formula f(t) = t^2. </summary>
    public static QuadraticEase QuadraticEase { get; set; } = new QuadraticEase();

    /// <summary> CubicEase: Creates an animation that accelerates and/or decelerates using the formula f(t) = t^3. </summary>
    public static CubicEase CubicEase { get; set; } = new CubicEase();
    /// <summary> QuarticEase: Creates an animation that accelerates and/or decelerates using the formula f(t) = t^4. </summary>
    public static QuarticEase QuarticEase { get; set; } = new QuarticEase();
    /// <summary> QuinticEase: Creates an animation that accelerates and/or decelerates using the formula f(t) = t^5. </summary>
    public static QuinticEase QuinticEase { get; set; } = new QuinticEase();

    /// <summary> ExponentialEase: Creates an animation that accelerates and/or decelerates using an exponential formula. </summary>
    public static ExponentialEase ExponentialEase { get; set; } = new ExponentialEase();

    /// <summary> SineEase: Creates an animation that accelerates and/or decelerates using a sine formula. </summary>
    public static SineEase SineEase { get; set; } = new SineEase();

}

1.3.4 Usage

/// <summary> Constructor </summary>
/// <param name="from"> Starting value</param>
/// <param name="to"> Ending value </param>
/// <param name="second"> Duration in seconds </param>
/// <param name="property"> Property name to modify </param>
///
public static DoubleStoryboardEngine Create(double from, double to, int second, string property)
{
    return new DoubleStoryboardEngine(from, to, second, property);
}

2. Property Form

Original title: Example: Simple ObjectProperyForm developed in WPF for binding entity forms
Original link: https://blog.csdn.net/u010975589/article/details/95970200

2.1 Purpose: Custom control to directly bind entity data, simplifying development cycles

2.2 Implementation

  1. Bind entity object
  2. Display property name via attributes
  3. Add validation conditions via attributes
  4. Currently implemented DataTemplates for String, Int, Double, DateTime, Bool simple types; other templates are extensible
  5. More updates to follow...

2.3 Example

Entity definition:

public class Student
{
    [Display("Name")]
    [Required]
    public string Name { get; set; }

    [Display("Class")]
    [Required]
    public string Class { get; set; }

    [Display("Address")]
    [Required]
    public string Address { get; set; }

    [Display("Email")]
    [Required]
    public string Emall { get; set; }

    [Display("Enabled")]
    [Required]
    public bool IsEnbled { get; set; }

    [Display("Time")]
    [Required]
    public DateTime time { get; set; }

    [Display("Age")]
    [Required]
    public int Age { get; set; }

    [Display("Average Score")]
    public double Score { get; set; }

    [Display("Phone Number")]
    [Required]
    [RegularExpression(@"^1[3|4|5|7|8][0-9]{9}$", ErrorMessage = "Invalid phone number!")]
    public string Tel { get; set; }
}
  • DisplayAttribute: used to identify the display name
  • RequiredAttribute: used to indicate that data cannot be empty
  • RegularExpression: references regex to validate data matching
  • Other attributes to be added later...

Usage:

<UserControl.Resources>
  <local:Student
    x:Key="S.Student.HeBianGu"
    Name="HeBianGu"
    Address="Gaoxin District, Chengdu City, Sichuan Province"
    Class="Grade 4"
    Emall="7777777777@QQ.com"
    Age="33"
    Score="99.99"
    IsEnbled="True"
    time="2019-09-09"
  />
</UserControl.Resources>

<wpfcontrollib:ObjectPropertyForm
  Grid.Row="1"
  Title="Student Information"
  SelectObject="{StaticResource S.Student.HeBianGu}"
>
  <base:Interaction.Behaviors>
    <base:MouseDragElementBehavior ConstrainToParentBounds="True" />
    <base:SelectZIndexElementBehavior /> </base:Interaction.Behaviors
></wpfcontrollib:ObjectPropertyForm>

2.4 Code

2.4.1 Get properties and attributes via reflection

ObservableCollection<ObjectPropertyItem> PropertyItemSource
{
    get { return (ObservableCollection<ObjectPropertyItem>)GetValue(PropertyItemSourceProperty); }
    set { SetValue(PropertyItemSourceProperty, value); }
}

// Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty PropertyItemSourceProperty =
    DependencyProperty.Register("PropertyItemSource", typeof(ObservableCollection<ObjectPropertyItem>), typeof(ObjectPropertyForm), new PropertyMetadata(new ObservableCollection<ObjectPropertyItem>(), (d, e) =>
      {
          ObjectPropertyForm control = d as ObjectPropertyForm;

          if (control == null) return;

          ObservableCollection<ObjectPropertyItem> config = e.NewValue as ObservableCollection<ObjectPropertyItem>;

      }));


void RefreshObject(object o)
{
    Type type = o.GetType();

    var propertys = type.GetProperties();

    this.PropertyItemSource.Clear();

    foreach (var item in propertys)
    {
        var from = ObjectPropertyFactory.Create(item, o);

        this.PropertyItemSource.Add(from);
    }

    this.ItemsSource = this.PropertyItemSource;
}

2.4.2 Define base type, extension classes, and factory method

/// <summary> Type base class </summary>
public class ObjectPropertyItem : NotifyPropertyChanged
{
    public string Name { get; set; }
    public PropertyInfo PropertyInfo { get; set; }

    public object Obj { get; set; }
    public ObjectPropertyItem(PropertyInfo property, object obj)
    {
        PropertyInfo = property;


        var display = property.GetCustomAttribute<DisplayAttribute>();

        Name = display == null ? property.Name : display.Name;

        Obj = obj;
    }


}

/// <summary> Generic type base class </summary>
public class ObjectPropertyItem<T> : ObjectPropertyItem
{
    private T _value;
    /// <summary> Description </summary>
    public T Value
    {
        get { return _value; }
        set
        {

            this.Message = null;

            //  Do: Validate data
            if (Validations != null)
            {
                foreach (var item in Validations)
                {
                    if (!item.IsValid(value))
                    {
                        this.Message = item.ErrorMessage;
                    }
                }
            }

            _value = value;

            RaisePropertyChanged("Value");

            this.SetValue(value);
        }
    }

    void SetValue(T value)
    {
        this.PropertyInfo.SetValue(Obj, value);
    }

    List<ValidationAttribute> Validations { get; }

    public ObjectPropertyItem(PropertyInfo property, object obj) : base(property, obj)
    {
        Value = (T)property.GetValue(obj);

        Validations = property.GetCustomAttributes<ValidationAttribute>()?.ToList();

        if(Validations!=null&& Validations.Count>0)
        {
            this.Flag = "*";
        }
    }



    private string _message;
    /// <summary> Description </summary>
    public string Message
    {
        get { return _message; }
        set
        {
            _message = value;
            RaisePropertyChanged("Message");
        }
    }

    public string Flag { get; set; }

}

/// <summary> String property type </summary>
public class StringPropertyItem : ObjectPropertyItem<string>
{
    public StringPropertyItem(PropertyInfo property, object obj) : base(property, obj)
    {
    }
}

/// <summary> DateTime property type </summary>
public class DateTimePropertyItem : ObjectPropertyItem<DateTime>
{
    public DateTimePropertyItem(PropertyInfo property, object obj) : base(property, obj)
    {
    }
}

/// <summary> Double property type </summary>
public class DoublePropertyItem : ObjectPropertyItem<double>
{
    public DoublePropertyItem(PropertyInfo property, object obj) : base(property, obj)
    {
    }
}

/// <summary> Int property type </summary>

public class IntPropertyItem : ObjectPropertyItem<int>
{
    public IntPropertyItem(PropertyInfo property, object obj) : base(property, obj)
    {
    }
}

/// <summary> Bool property type </summary>
public class BoolPropertyItem : ObjectPropertyItem<bool>
{
    public BoolPropertyItem(PropertyInfo property, object obj) : base(property, obj)
    {
    }
}

Type factory:

public class ObjectPropertyFactory
{
    public static ObjectPropertyItem Create(PropertyInfo info, object obj)
    {
        if (info.PropertyType == typeof(int))
        {
            return new IntPropertyItem(info, obj);
        }
        else if (info.PropertyType == typeof(string))
        {
            return new StringPropertyItem(info, obj);
        }
        else if (info.PropertyType == typeof(DateTime))
        {
            return new DateTimePropertyItem(info, obj);
        }
        else if (info.PropertyType == typeof(double))
        {
            return new DoublePropertyItem(info, obj);
        }
        else if (info.PropertyType == typeof(bool))
        {
            return new BoolPropertyItem(info, obj);
        }

        return null;
    }
}

2.4.3 Style templates

<DataTemplate DataType="{x:Type base:StringPropertyItem}">
  <Grid
    Width="{Binding RelativeSource={RelativeSource AncestorType=local:ObjectPropertyForm},Path=Width-5}"
    Height="35"
    Margin="5,0"
  >
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="2*" />
      <ColumnDefinition Width="30" />
    </Grid.ColumnDefinitions>

    <TextBlock
      Text="{Binding Name}"
      FontSize="14"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
    />

    <TextBlock
      Text="{Binding Flag}"
      Grid.Column="1"
      Margin="5,0"
      FontSize="14"
      Foreground="{DynamicResource S.Brush.Red.Notice}"
      HorizontalAlignment="Right"
      VerticalAlignment="Center"
    />

    <local:FTextBox
      Text="{Binding Value,UpdateSourceTrigger=PropertyChanged}"
      Style="{DynamicResource DefaultTextBox}"
      FontSize="14"
      Width="Auto"
      CaretBrush="Black"
      Grid.Column="2"
      Height="30"
      base:ControlAttachProperty.FIcon=""
      VerticalContentAlignment="Center"
      HorizontalAlignment="Stretch"
      VerticalAlignment="Center"
    />

    <TextBlock
      Text="&#xe626;"
      Grid.Column="3"
      Style="{DynamicResource FIcon }"
      Foreground="{DynamicResource S.Brush.Red.Notice}"
      Visibility="{Binding Message,Converter={x:Static base:XConverter.VisibilityWithOutStringConverter},ConverterParameter={x:Null},Mode=TwoWay}"
      FontSize="14"
      TextTrimming="CharacterEllipsis"
      ToolTip="{Binding Message}"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
    />
  </Grid>
</DataTemplate>

<DataTemplate DataType="{x:Type base:BoolPropertyItem}">
  <Grid
    Width="{Binding RelativeSource={RelativeSource AncestorType=local:ObjectPropertyForm},Path=Width-5}"
    Height="35"
    Margin="5,0"
  >
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="2*" />
      <ColumnDefinition Width="30" />
    </Grid.ColumnDefinitions>

    <TextBlock
      Text="{Binding Name}"
      FontSize="14"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
    />

    <TextBlock
      Text="{Binding Flag}"
      Grid.Column="1"
      Margin="5,0"
      FontSize="14"
      Foreground="{DynamicResource S.Brush.Red.Notice}"
      HorizontalAlignment="Right"
      VerticalAlignment="Center"
    />
    <CheckBox
      IsChecked="{Binding Value}"
      FontSize="14"
      Grid.Column="2"
      Height="30"
      VerticalContentAlignment="Center"
      HorizontalAlignment="Left"
      VerticalAlignment="Center"
    />

    <TextBlock
      Text="&#xe626;"
      Grid.Column="3"
      Style="{DynamicResource FIcon }"
      Foreground="{DynamicResource S.Brush.Red.Notice}"
      Visibility="{Binding Message,Converter={x:Static base:XConverter.VisibilityWithOutStringConverter},ConverterParameter={x:Null}}"
      FontSize="14"
      TextTrimming="CharacterEllipsis"
      ToolTip="{Binding Message}"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
    />
  </Grid>
</DataTemplate>

<DataTemplate DataType="{x:Type base:DateTimePropertyItem}">
  <Grid
    Width="{Binding RelativeSource={RelativeSource AncestorType=local:ObjectPropertyForm},Path=Width-5}"
    Height="35"
    Margin="5,0"
  >
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="2*" />
      <ColumnDefinition Width="30" />
    </Grid.ColumnDefinitions>

    <TextBlock
      Text="{Binding Name}"
      FontSize="14"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
    />

    <TextBlock
      Text="{Binding Flag}"
      Grid.Column="1"
      Margin="5,0"
      FontSize="14"
      Foreground="{DynamicResource S.Brush.Red.Notice}"
      HorizontalAlignment="Right"
      VerticalAlignment="Center"
    />
    <DatePicker
      SelectedDate="{Binding Value}"
      FontSize="14"
      Grid.Column="2"
      Height="30"
      VerticalContentAlignment="Center"
      Width="Auto"
      HorizontalAlignment="Stretch"
      VerticalAlignment="Center"
    />

    <TextBlock
      Text="&#xe626;"
      Grid.Column="3"
      Style="{DynamicResource FIcon }"
      Foreground="{DynamicResource S.Brush.Red.Notice}"
      Visibility="{Binding Message,Converter={x:Static base:XConverter.VisibilityWithOutStringConverter},ConverterParameter={x:Null}}"
      FontSize="14"
      TextTrimming="CharacterEllipsis"
      ToolTip="{Binding Message}"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
    />
  </Grid>
</DataTemplate>

<DataTemplate DataType="{x:Type base:IntPropertyItem}">
  <Grid
    Width="{Binding RelativeSource={RelativeSource AncestorType=local:ObjectPropertyForm},Path=Width-5}"
    Height="35"
    Margin="5,0"
  >
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="2*" />
      <ColumnDefinition Width="30" />
    </Grid.ColumnDefinitions>

    <TextBlock
      Text="{Binding Name}"
      FontSize="14"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
    />

    <TextBlock
      Text="{Binding Flag}"
      Grid.Column="1"
      Margin="5,0"
      FontSize="14"
      Foreground="{DynamicResource S.Brush.Red.Notice}"
      HorizontalAlignment="Right"
      VerticalAlignment="Center"
    />
    <Slider
      Value="{Binding Value}"
      FontSize="14"
      Grid.Column="2"
      Height="30"
      VerticalContentAlignment="Center"
      HorizontalAlignment="Stretch"
      VerticalAlignment="Center"
    />

    <TextBlock
      Text="&#xe626;"
      Grid.Column="3"
      Style="{DynamicResource FIcon }"
      Foreground="{DynamicResource S.Brush.Red.Notice}"
      Visibility="{Binding Message,Converter={x:Static base:XConverter.VisibilityWithOutStringConverter},ConverterParameter={x:Null}}"
      FontSize="14"
      TextTrimming="CharacterEllipsis"
      ToolTip="{Binding Message}"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
    />
  </Grid>
</DataTemplate>

<DataTemplate DataType="{x:Type base:DoublePropertyItem}">
  <Grid
    Width="{Binding RelativeSource={RelativeSource AncestorType=local:ObjectPropertyForm},Path=Width-5}"
    Height="35"
    Margin="5,0"
  >
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="2*" />
      <ColumnDefinition Width="30" />
    </Grid.ColumnDefinitions>

    <TextBlock
      Text="{Binding Name}"
      FontSize="14"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
    />

    <TextBlock
      Text="{Binding Flag}"
      Grid.Column="1"
      Margin="5,0"
      FontSize="14"
      Foreground="{DynamicResource S.Brush.Red.Notice}"
      HorizontalAlignment="Right"
      VerticalAlignment="Center"
    />
    <Slider
      Value="{Binding Value}"
      FontSize="14"
      Grid.Column="2"
      Height="30"
      VerticalContentAlignment="Center"
      HorizontalAlignment="Stretch"
      VerticalAlignment="Center"
    />

    <TextBlock
      Text="&#xe626;"
      Grid.Column="3"
      Style="{DynamicResource FIcon }"
      Foreground="{DynamicResource S.Brush.Red.Notice}"
      Visibility="{Binding Message,Converter={x:Static base:XConverter.VisibilityWithOutStringConverter},ConverterParameter={x:Null}}"
      FontSize="14"
      TextTrimming="CharacterEllipsis"
      ToolTip="{Binding Message}"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
    />
  </Grid>
</DataTemplate>

<style TargetType="local:ObjectPropertyForm">
  <Setter Property="Background" Value="{DynamicResource S.Brush.TextBackgroud.Default}"/>
  <Setter Property="BorderThickness" Value="0"/>
  <!--<Setter Property="BorderBrush" Value="{x:Null}"/>-->
  <Setter Property="HorizontalAlignment" Value="Stretch"/>
  <Setter Property="VerticalAlignment" Value="Center"/>
  <Setter Property="HorizontalContentAlignment" Value="Center"/>
  <Setter Property="VerticalContentAlignment" Value="Center"/>
  <!--<Setter Property="FocusVisualStyle" Value="{x:Null}"/>-->
  <Setter Property="Padding" Value="0" />
  <Setter Property="Width" Value="500" />
  <Setter Property="Height" Value="Auto" />
  <Setter Property="ItemsSource" Value="{Binding PropertyItemSource,Mode=TwoWay}" />
  <Setter Property="ItemsPanel">
      <Setter.Value>
          <ItemsPanelTemplate>
              <StackPanel/>

          </ItemsPanelTemplate>
      </Setter.Value>
  </Setter>
  <Setter Property="Template">
      <Setter.Value>
          <ControlTemplate TargetType="local:ObjectPropertyForm">
              <GroupBox Header="{TemplateBinding Title}">
                  <Border HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                      VerticalAlignment="{TemplateBinding VerticalAlignment}"
                      Background="{TemplateBinding Background}"
                      BorderBrush="{TemplateBinding BorderBrush}"
                      BorderThickness="{TemplateBinding BorderThickness}">
                      <ItemsPresenter/>
                  </Border>
              </GroupBox>
          </ControlTemplate>
      </Setter.Value>
  </Setter>
</style>

2.4.4 Extensibility

2.4.4.1 Just define an extension type, e.g.:
/// <summary> String property type </summary>
public class StringPropertyItem : ObjectPropertyItem<string>
{
    public StringPropertyItem(PropertyInfo property, object obj) : base(property, obj)
    {
    }
}
2.4.4.2 Then add a DataTemplate, e.g.:
<DataTemplate DataType="{x:Type base:StringPropertyItem}">
  <Grid
    Width="{Binding RelativeSource={RelativeSource AncestorType=local:ObjectPropertyForm},Path=Width-5}"
    Height="35"
    Margin="5,0"
  >
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="2*" />
      <ColumnDefinition Width="30" />
    </Grid.ColumnDefinitions>

    <TextBlock
      Text="{Binding Name}"
      FontSize="14"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
    />

    <TextBlock
      Text="{Binding Flag}"
      Grid.Column="1"
      Margin="5,0"
      FontSize="14"
      Foreground="{DynamicResource S.Brush.Red.Notice}"
      HorizontalAlignment="Right"
      VerticalAlignment="Center"
    />

    <local:FTextBox
      Text="{Binding Value,UpdateSourceTrigger=PropertyChanged}"
      Style="{DynamicResource DefaultTextBox}"
      FontSize="14"
      Width="Auto"
      CaretBrush="Black"
      Grid.Column="2"
      Height="30"
      base:ControlAttachProperty.FIcon=""
      VerticalContentAlignment="Center"
      HorizontalAlignment="Stretch"
      VerticalAlignment="Center"
    />

    <TextBlock
      Text="&#xe626;"
      Grid.Column="3"
      Style="{DynamicResource FIcon }"
      Foreground="{DynamicResource S.Brush.Red.Notice}"
      Visibility="{Binding Message,Converter={x:Static base:XConverter.VisibilityWithOutStringConverter},ConverterParameter={x:Null},Mode=TwoWay}"
      FontSize="14"
      TextTrimming="CharacterEllipsis"
      ToolTip="{Binding Message}"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
    />
  </Grid>
</DataTemplate>

3. Message Dialog

Original title: Example: Custom MessageService in WPF using DialogHost, Snackbar, NotifyIcon to display various scenario prompts
Original link: https://blog.csdn.net/u010975589/article/details/95985190

3.1 Purpose

Different interaction scenarios require different messages, and different messages need different effects to display. Use DialogHost, NotifyIcon, Snackbar to display various scenario prompt messages, applicable in ViewModel.

3.2 Implementation

  1. Waiting dialog
  2. Confirmation dialog
  3. Confirm and cancel dialog
  4. Percentage progress and text progress dialog
  5. Toast notification (NotifyIcon)
  6. Snackbar message

3.3 Example

Description:

  1. Dialog: Conventional dialog messages as shown above, waiting dialog, message dialog, progress dialog;

(Currently only these types are encapsulated; custom dialogs only require creating a user control and calling the general loading method. More updates to come...)

  1. Prompt message: When progress saves successfully, a prompt message is needed, displayed for 2s and then auto-hides (as shown in the "Friendly Tip" section);

  2. Toast notification: When the program is in a hidden or certain state, a toast notification message is needed;

3.4 Code

[ViewModel("Loyout")]
class LoyoutViewModel : MvcViewModelBase
{


    /// <summary> General command method </summary>
    protected override async void RelayMethod(object obj)

    {
        string command = obj?.ToString();

        //  Do: Dialog message
        if (command == "Button.ShowDialogMessage")
        {
            await MessageService.ShowSumitMessge("This is a message dialog?");

        }
        //  Do: Waiting message
        else if (command == "Button.ShowWaittingMessge")
        {

            await MessageService.ShowWaittingMessge(() => Thread.Sleep(2000));

        }
        //  Do: Percentage progress dialog
        else if (command == "Button.ShowPercentProgress")
        {
            Action<IPercentProgress> action = l =>
                {
                    for (int i = 0; i < 100; i++)
                    {
                        l.Value = i;

                        Thread.Sleep(50);
                    }

                    Thread.Sleep(1000);

                    MessageService.ShowSnackMessageWithNotice("Loading complete!");
                };
            await MessageService.ShowPercentProgress(action);

        }
        //  Do: Text progress dialog
        else if (command == "Button.ShowStringProgress")
        {
            Action<IStringProgress> action = l =>
            {
                for (int i = 1; i <= 100; i++)
                {
                    l.MessageStr = $"Submitting page {i} of 100 data items";

                    Thread.Sleep(50);
                }

                Thread.Sleep(1000);

                MessageService.ShowSnackMessageWithNotice("Submission complete: 100 succeeded, 0 failed!");
            };

            await MessageService.ShowStringProgress(action);

        }
        //  Do: Confirm/cancel dialog
        else if (command == "Button.ShowResultMessge")
        {
            Action<object, DialogClosingEventArgs> action = (l, k) =>
            {
                if ((bool)k.Parameter)
                {
                    MessageService.ShowSnackMessageWithNotice("You clicked cancel");
                }
                else
                {
                    MessageService.ShowSnackMessageWithNotice("You clicked confirm");
                }
            };

            MessageService.ShowResultMessge("Are you sure you want to exit the system?", action);


        }
        //  Do: Prompt message
        else if (command == "Button.ShowSnackMessage")
        {
            MessageService.ShowSnackMessageWithNotice("This is a prompt message?");
        }
        //  Do: Toast notification
        else if (command == "Button.ShowNotifyMessage")
        {
            MessageService.ShowNotifyMessage("You have an alarm message to handle, please check", "Notify By HeBianGu");
        }
    }
}

4. Applying MVC in WPF

Original title: Encapsulation: Brief introduction to custom MVC framework based on WPF
Original link: https://blog.csdn.net/u010975589/article/details/100019431

4.1 Purpose

When using ASP.NET Core, I deeply appreciate the convenience of the MVC framework for page navigation and data processing. However, there seems to be no ready-made MVC framework in WPF. Therefore, I developed a custom MVC framework and experienced its advantages during use. Below is a brief introduction to this MVC framework based on MVVM.

4.2 Project Structure

It mainly consists of three parts: Controller, View, ViewModel

Where View and ViewModel follow the traditional WPF MVVM pattern.

The difference lies in page navigation, which is controlled by the Controller. Here is an example of a Controller definition.

4.3 Controller Structure and Definition

4.3.1 Define LoyoutController

[Route("Loyout")]
class LoyoutController : Controller
{

    public LoyoutController(ShareViewModel shareViewModel) : base(shareViewModel)
    {

    }

    public async Task<IActionResult> Center()
    {
        return View();
    }

    [Route("OverView/Button")]
    public async Task<IActionResult> Mdi()
    {
        return View();
    }

    public async Task<IActionResult> Left()
    {
        return View();
    }

    public async Task<IActionResult> Right()
    {
        return View();
    }

    public async Task<IActionResult> Top()
    {
        return View();
    }

    public async Task<IActionResult> Bottom()
    {
        return View();
    }

    [Route("OverView/Toggle")]
    public async Task<IActionResult> Toggle()
    {
        return View();
    }

    [Route("OverView/Carouse")]
    public async Task<IActionResult> Carouse()
    {
        return View();
    }

    [Route("OverView/Evaluate")]
    public async Task<IActionResult> Evaluate()
    {
        return View();
    }

    [Route("OverView/Expander")]
    public async Task<IActionResult> Expander()
    {
        return View();
    }

    [Route("OverView/Gif")]
    public async Task<IActionResult> Gif()
    {
        return View();
    }

    [Route("OverView/Message")]
    public async Task<IActionResult> Message()
    {
        return View();
    }

    [Route("OverView/Upgrade")]
    public async Task<IActionResult> Upgrade()
    {
        return View();
    }

    [Route("OverView/Property")]
    public async Task<IActionResult> Property()
    {
        return View();
    }

    [Route("OverView/ProgressBar")]
    public async Task<IActionResult> ProgressBar()
    {
        return View();
    }

    [Route("OverView/Slider")]
    public async Task<IActionResult> Slider()
    {
        return View();
    }

    [Route("OverView/Tab")]
    public async Task<IActionResult> Tab()
    {
        return View();
    }

    [Route("OverView/Tree")]
    public async Task<IActionResult> Tree()
    {
        return View();
    }

    [Route("OverView/Observable")]
    public async Task<IActionResult> Observable()
    {
        return View();
    }

    [Route("OverView/Brush")]
    public async Task<IActionResult> Brush()
    {
        return View();
    }

    [Route("OverView/Shadow")]
    public async Task<IActionResult> Shadow()
    {
        return View();
    }

    [Route("OverView/Button")]
    public async Task<IActionResult> Button()
    {
        await MessageService.ShowWaittingMessge(() => Thread.Sleep(500));

        this.ViewModel.ButtonContentText = DateTime.Now.ToString();

        return View();

    }



    [Route("OverView/Grid")]
    public async Task<IActionResult> Grid()
    {
        return View();
    }

    [Route("OverView/Combobox")]
    public async Task<IActionResult> Combobox()
    {
        return View();
    }

    [Route("OverView")]
    public async Task<IActionResult> OverView()
    {
        await MessageService.ShowWaittingMessge(() => Thread.Sleep(500));

        MessageService.ShowSnackMessageWithNotice("OverView");

        return View();
    }

    [Route("OverView/TextBox")]
    public async Task<IActionResult> TextBox()
    {
        return View();
    }

    [Route("OverView/Book")]
    public async Task<IActionResult> Book()
    {
        return View();
    }

    [Route("OverView/Xaml")]
    public async Task<IActionResult> Xaml()
    {
        return View();
    }

    [Route("OverView/Dimension")]
    public async Task<IActionResult> Dimension()
    {
        return View();
    }

    [Route("OverView/Geometry")]
    public async Task<IActionResult> Geometry()
    {
        return View();
    }

    [Route("OverView/Panel")]
    public async Task<IActionResult> Panel()
    {
        return View();
    }
    [Route("OverView/Transform3D")]
    public async Task<IActionResult> Transform3D()
    {
        return View();
    }

    [Route("OverView/Drawer")]
    public async Task<IActionResult> Drawer()
    {
        return View();
    }
}

4.3.2 Frontend Page

As shown below, the red part corresponds to the Route to be navigated in the Controller.

For example, when the red "Button" is selected, the Button() method is called first, and then the page navigates to the ButtonControl.xaml file under the corresponding View folder of the current Controller.

[Route("OverView/Button")]
public async Task<IActionResult> Button()
{
    await MessageService.ShowWaittingMessge(() => Thread.Sleep(500);

    this.ViewModel.ButtonContentText = DateTime.Now.ToString();

    return View();

}

You can write business logic in the Button() method, such as CRUD operations on the current ViewModel. The ViewModel member in the current Controller is the internally encapsulated ViewModel, which corresponds to the ViewModel of the current Controller under the ViewModel folder.

4.3.3 Example

4.3.4 The Xaml list on the left can be defined as follows

<Grid>
  <wpfcontrollib:LinkGroupExpander
    ScrollViewer.HorizontalScrollBarVisibility="Disabled"
    x:Name="selectloyout"
    SelectedLink="{Binding SelectLink,Mode=TwoWay}"
    Command="{x:Static wpfcontrollib:DrawerHost.CloseDrawerCommand}"
    CommandParameter="{x:Static Dock.Left}"
  >
    <wpfcontrollib:LinkActionGroup DisplayName="Basic Controls" Logo="&#xe69f;">
      <wpfcontrollib:LinkActionGroup.Links>
        <wpfcontrollib:LinkAction
          DisplayName="Button"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Button"
        />
        <wpfcontrollib:LinkAction
          DisplayName="TextBox"
          Logo="&#xe6a3;"
          Controller="Loyout"
          Action="TextBox"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Combobox"
          Logo="&#xe6a3;"
          Controller="Loyout"
          Action="Combobox"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Toggle"
          Logo="&#xe6a3;"
          Controller="Loyout"
          Action="Toggle"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Evaluate"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Evaluate"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Expander"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Expander"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Gif"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Gif"
        />
        <wpfcontrollib:LinkAction
          DisplayName="ProgressBar"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="ProgressBar"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Slider"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Slider"
        />
      </wpfcontrollib:LinkActionGroup.Links>
    </wpfcontrollib:LinkActionGroup>

    <wpfcontrollib:LinkActionGroup DisplayName="Layout Controls" Logo="&#xe69f;">
      <wpfcontrollib:LinkActionGroup.Links>
        <wpfcontrollib:LinkAction
          DisplayName="MdiControl"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Mdi"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Carouse"
          Logo="&#xe69e;"
          Controller="Loyout"
          Action="Carouse"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Tab"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Tab"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Tree"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Tree"
        />
        <wpfcontrollib:LinkAction
          DisplayName="ObservableSource"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Observable"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Property"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Property"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Panel"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Panel"
        />
      </wpfcontrollib:LinkActionGroup.Links>
    </wpfcontrollib:LinkActionGroup>

    <wpfcontrollib:LinkActionGroup DisplayName="Global Controls" Logo="&#xe69f;">
      <wpfcontrollib:LinkActionGroup.Links>
        <wpfcontrollib:LinkAction
          DisplayName="Message"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Message"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Upgrade"
          Logo="&#xe69e;"
          Controller="Loyout"
          Action="Upgrade"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Drawer"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Drawer"
        />
      </wpfcontrollib:LinkActionGroup.Links>
    </wpfcontrollib:LinkActionGroup>

    <wpfcontrollib:LinkActionGroup DisplayName="Global Styles" Logo="&#xe69f;">
      <wpfcontrollib:LinkActionGroup.Links>
        <wpfcontrollib:LinkAction
          DisplayName="Brush"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Brush"
        />
        <wpfcontrollib:LinkAction
          DisplayName="Shadow"
          Logo="&#xe69f;"
          Controller="Loyout"
          Action="Shadow"
        />
      </wpfcontrollib:LinkActionGroup.Links>
    </wpfcontrollib:LinkActionGroup>
  </wpfcontrollib:LinkGroupExpander>
</Grid>

Through the LinkGroupExpander control, encapsulate LinkAction to achieve page navigation. Only need to define a few properties of LinkAction to navigate to the specified page, for example:

  • Controller property: indicates which Controller to navigate to
  • Action property: indicates which method to navigate to
  • DisplayName property: name displayed in the UI
  • Logo property: icon displayed in the UI

For example, the configuration for the Button() method in the Controller is as follows:

[Route("OverView/Button")]
public async Task<IActionResult> Button()
<wpfcontrollib:LinkAction
  DisplayName="Button"
  Logo="&#xe69f;"
  Controller="Loyout"
  Action="Button"
/>

4.3.5 ControllerBase Definition

The main method is IActionResult View([CallerMemberName] string name = ""). This method is the core of the MVC implementation. It uses reflection to dynamically load the assembly, load the View and ViewModel from the project structure, and generate an IActionResult to return to the main page for navigation. The code is as follows:

public abstract class ControllerBase : IController
{
    protected virtual IActionResult View([CallerMemberName] string name = "")
    {
        var route = this.GetType().GetCustomAttributes(typeof(RouteAttribute), true).Cast<RouteAttribute>();

        string controlName = null;

        if (route.FirstOrDefault() == null)
        {
            controlName = this.GetType().Name;
        }
        else
        {
            controlName = route.FirstOrDefault().Name;
        }

        var ass = Assembly.GetEntryAssembly().GetName();

        string path = $"/{ass.Name};component/View/{controlName}/{name}Control.xaml";

        Uri uri = new Uri(path, UriKind.RelativeOrAbsolute);

        var content = Application.Current.Dispatcher.Invoke(() =>
        {
            return Application.LoadComponent(uri);
        });

        ActionResult result = new ActionResult();

        result.Uri = uri;
        result.View = content as ContentControl;

        Type type = Assembly.GetEntryAssembly().GetTypeOfMatch<NotifyPropertyChanged>(l => l.Name == controlName + "ViewModel");

        result.ViewModel = ServiceRegistry.Instance.GetInstance(type);

        Application.Current.Dispatcher.Invoke(() =>
        {
            (result.View as FrameworkElement).DataContext = result.ViewModel;

        });

        return result;
    }


    protected virtual IActionResult LinkAction([CallerMemberName] string name = "")
    {
        var route = this.GetType().GetCustomAttributes(typeof(RouteAttribute), true).Cast<RouteAttribute>();

        string controlName = null;

        if (route.FirstOrDefault() == null)
        {
            controlName = this.GetType().Name;
        }
        else
        {
            controlName = route.FirstOrDefault().Name;
        }

        var ass = Assembly.GetEntryAssembly().GetName();

        string path = $"/{ass.Name};component/View/{controlName}/{name}Control.xaml";

        Uri uri = new Uri(path, UriKind.RelativeOrAbsolute);

        var content = Application.Current.Dispatcher.Invoke(() =>
        {
            return Application.LoadComponent(uri);
        });

        ActionResult result = new ActionResult();

        result.Uri = uri;
        result.View = content;

        Type type = Assembly.GetEntryAssembly().GetTypeOfMatch<NotifyPropertyChanged>(l => l.Name == controlName + "ViewModel");

        result.ViewModel = ServiceRegistry.Instance.GetInstance(type);

        Application.Current.Dispatcher.Invoke(() =>
        {
            (result.View as FrameworkElement).DataContext = result.ViewModel;
        });

        return result;
    }

}

Description:

  1. Load and generate the Control via Application.LoadComponent(uri);
  2. Find the corresponding ViewModel via reflection on the ViewModel base class NotifyPropertyChanged, and bind it to the View;
  3. Encapsulate the View and ViewModel into IActionResult and return it to the main page for loading.

The method return type in the Controller is async Task, meaning all page navigation is performed asynchronously, effectively avoiding freezes during page switching.

4.4 View Structure and Definition

The View in the project corresponds to the methods in the Controller. It must strictly follow the structure [View/Loyout] in MVC. The benefit is reduced code amount and uniform code structure. The structure is as follows:

The red ButtonControl.xaml is the page that the Button() method in the Controller navigates to. Other pages follow the same logic.

4.5 ViewModel Structure and Definition

LoyoutViewModel corresponds to LoyoutController and all pages under View/Loyout.

4.6 Overall MVC Structure Effect

The above is a brief example of applying MVC in WPF. For details and examples, download the code from the following links.

Code repository: https://github.com/HeBianGu/WPF-ControlBase.git

Another example using Sqlite database is as follows:

Code repository: https://github.com/HeBianGu/WPF-ExplorerManager.git

5. Other Feature Descriptions

Original title: Example: Custom WPF base control UI library HeBianGu.General.WpfControlLib V2.0
Original link: https://blog.csdn.net/u010975589/article/details/103083605

5.1 Purpose

Encapsulate some controls into a custom control library for rapid development.

5.2 Implemented Features

  • Basically implemented commonly used basic controls to meet rapid development of conventional software
  • Supports .NET Core 3.0+ and .NET Framework 4.5+

5.3 Overall Overview

5.3.1 Login Page

The login page only needs to inherit the LoginWindowBase base class and set the style to Style="{StaticResource S.Window.Login.Default}".

5.3.2 Main Page

The main page only needs to inherit the LinkWindowBase base class and set the style to Style="{DynamicResource S.Window.Link.Default}".

The entire main window uses a ViewBox layout, so it is compatible when scaled or applied to other resolution devices.

5.3.3 Theme Configuration Saving

Theme configuration information has been encapsulated in ApplicationBase. It will automatically save the set configuration (such as theme color, font size, etc.) when exiting.

Summary: Applying this pattern achieves reusability by encapsulating common parts in the base layer. To modify the style, simply modify the Style file or change dependency properties.

5.4 Theme Settings

Light theme example:

Dark theme example:

Theme settings mainly include:

  1. Set primary theme color

The primary color is used to highlight the parts to be emphasized. Currently, you can choose built-in colors, follow the system theme color, customize a color, or use a dynamic theme (i.e., set the theme to automatically change at specified intervals).

  1. Set theme

Currently four themes are implemented: light, dark, gray, and primary color theme.

  1. Set font size

Two built-in font sizes: Large and Small. These two are loaded via injection, meaning the initial values can be set when the program loads.

  1. Other configurations

Includes Chinese/English, setting standard row height, etc., which can be initialized at program load. Not detailed here.

Summary: The design intent is that aesthetics vary from person to person; using custom configuration can meet changing needs as much as possible.

5.5 Other Basic Controls

5.5.1 Data Grid

  • a Compatible with theme font and settings; all subsequent controls have applied theme settings, no further explanation.
  • b Rows per page

You can set the number of rows displayed per page.

  • c Search

You can set search filter criteria; only items containing the specified search term will be displayed.

  • d Page navigation

Previous page, next page, first page, last page, specific page.

  • e Page information

The range of items currently displayed (from which to which) and the total number of items in the data source.

  • f Two styles of grid pages

Summary: The above functions are encapsulated in the PagedDataGrid control. Simply bind the data source to achieve the above features. Printing and export functions are not yet implemented.

5.5.2 Tree List

  • a Supports filtering by category

As shown above, select a specific type to filter the list.

  • b Supports conditional search

As shown above, enter a condition to filter specific criteria.

Summary: Usage is to bind the data source to the TreeListView control.

5.5.3 Other Common Controls

  • a Dialog

Uses built-in dialog, not a window, but an overlay layer, avoiding some issues caused by window dialogs.

  • b Custom dialog window

More aesthetically pleasing than system dialogs, with show/hide animations. By injection, you can customize the number and functionality of buttons.

  • c Message list

Currently two modes: displayed within the window and in the Windows system. You can customize the display method based on requirements. Example below:

  • d Online upgrade example

  • e Navigation menu example

  • f Other features include

Button control, text input box control, dropdown list control, numeric control, date picker control, bindable password box control, progress bar control, drag control, tree control, pagination control, and other custom controls.

All the above controls support theme color and font size switching, suitable for common software functionality.

The overall structure uses a custom MVC loading method. Reference: https://blog.csdn.net/u010975589/article/details/100019431

Due to the large number of controls, not detailed here. Interested parties can download the source code or install the NuGet package.

5.6 Usage

Add the NuGet package as shown below:

Note: Some parts of this example reference third-party frameworks. The open source is only for learning and reference, not for commercial purposes.

Other examples using this framework:

GitHub download: GitHub - HeBianGu/WPF-ControlBase: Wpf encapsulated custom control resource library

Example installer download:

Update: 2019.12.16 Added .NET Core 3.0

Now supports Core3.0 and .NET 4.5. If the solution assembly fails to load, please install these two frameworks.

Keep Exploring

Related Reading

More Articles
Same category / Same tag 9/13/2025

Migration Series from WPF to Avalonia: Why I Must Migrate My WPF Application to Avalonia

In the past few years, our host computer software has mainly been developed using WPF and WinForm . These technologies work well on the Windows platform and have accompanied us from small-scale trial production to the current stage of large-scale delivery. However, with business development and changes in customer requirements, the single Windows technology stack has gradually become a hurdle we must overcome.

Continue Reading
Same category / Same tag 1/26/2025

Implementing Internationalization in WPF Using Custom XML Files

This article details the method of implementing internationalization in WPF applications using custom XML files, including installing the necessary NuGet packages, dynamically retrieving the language list, dynamically switching languages, using translated strings in code and XAML interfaces, and provides a source code link to help developers easily achieve internationalization in WPF applications.

Continue Reading