NETworkManager源码剖析:一个网络管理工具的实现细节

在 GitHub 闲逛时,我偶然发现了NETworkManager这个开源项目。作为一款网络管理和故障排查工具,它已经积累了超过 8.5k Star,足以说明社区对它的认可。更让我感兴趣的是,这个项目采用 C# + WPF 进行开发,与我当前正在深入学习和实践的技术栈高度契合。因此,我产生了一个想法:通过深入分析 NETworkManager 的源码,探索一个成熟的 WPF 桌面应用是如何进行架构设计、模块划分以及工程实践的。接下来,我将以 NETworkManager 为切入点,逐步拆解它在 WPF UI 设计、MVVM 架构、依赖注入、配置管理、网络通信、异步编程以及工程化实践 等方面的实现,希望能够从优秀的开源项目中学习桌面应用开发的最佳实践。

1
git clone --recurse-submodules https://github.com/BornToBeRoot/NETworkManager.git

将项目Clone到本地,整个解决方案结构如下:

图1 NETworkManager项目整体结构
图1 NETworkManager项目整体结构

解决方案下包含了多个子项目,说明将实现逻辑进行了合理拆分,进行了模块化设计。

1. NETworkManager启动时,到底发生了什么?

WPF应用生命周期大致如下:

图表 代码
flowchart TD
    A[Application_Startup] --> B[创建Window]
    B --> C[用户正常使用]
    C --> D[Application_Exit]
    D --> E[程序结束]
flowchart TD
    A[Application_Startup] --> B[创建Window]
    B --> C[用户正常使用]
    C --> D[Application_Exit]
    D --> E[程序结束]
flowchart TD
    A[Application_Startup] --> B[创建Window]
    B --> C[用户正常使用]
    C --> D[Application_Exit]
    D --> E[程序结束]
1
2
3
4
5
flowchart TD
    A[Application_Startup] --> B[创建Window]
    B --> C[用户正常使用]
    C --> D[Application_Exit]
    D --> E[程序结束]

在Visual Studio 2026按F5调试启动后,首先映入眼帘的是启动画面窗口:

图2 NETworkManager启动画面窗口
图2 NETworkManager启动画面窗口

接着就会出现隐私政策设置界面:

图3 隐私政策设置界面
图3 隐私政策设置界面

点击“继续”后就进入到了主界面(仪表盘):

图4 NETworkManager主界面
图4 NETworkManager主界面

NETworkManager项目的App.xaml.cs中包含了软件启动时的逻辑,里面主要包含了Application_StartupOnSessionEndingApplication_ExitSave等4个方法。

Application_Startup

设置了全局异常捕获:

1
2
3
4
5
6
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
{
    Log.Fatal("Unhandled exception occured!");

    Log.Fatal($"Exception raised by: {args.ExceptionObject}");
};

等待旧进程退出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if (CommandLineManager.Current.RestartPid != -1)
{
    Log.Info(
        $"Waiting for another NETworkManager process with Pid {CommandLineManager.Current.RestartPid} to exit...");

    var processList = Process.GetProcesses();
    var process = processList.FirstOrDefault(x => x.Id == CommandLineManager.Current.RestartPid);
    process?.WaitForExit();

    Log.Info($"NETworkManager process with Pid {CommandLineManager.Current.RestartPid} has been exited.");
}

单实例检测:

1
_mutex = new Mutex(true, "{" + Guid + "}");

防止用户点击两次exe,两个程序运行。第一次创建Mutex成功,第二次创建Mutex失败。于是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var mutexIsAcquired = _mutex.WaitOne(TimeSpan.Zero, true);

Log.Info($"Mutex value for {Guid} is {mutexIsAcquired}");

// Release mutex
if (mutexIsAcquired)
    _mutex.ReleaseMutex();

// If another instance is running, bring it to the foreground
if (!mutexIsAcquired && !SettingsManager.Current.Window_MultipleInstances)
{
    // Bring the already running application into the foreground
    Log.Info(
        "Another NETworkManager process is already running. Trying to bring the window to the foreground...");
    SingleInstance.PostMessage(SingleInstance.HWND_BROADCAST, SingleInstance.WM_SHOWME, IntPtr.Zero,
        IntPtr.Zero);

    // Close the application
    _singleInstanceClose = true;
    Shutdown();

    return;
}

接下来会显示启动画面窗口和主窗口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Show splash screen
if (SettingsManager.Current.SplashScreen_Enabled)
{
    Log.Info("Show SplashScreen while application is loading...");
    new SplashScreen(@"SplashScreen.png").Show(true, true);
}

// Show main window
Log.Info("Set StartupUri to MainWindow.xaml...");
StartupUri = new Uri("MainWindow.xaml", UriKind.Relative);

启动流程如下:

图表 代码
flowchart TD
    A[Application_Startup] --> B[启动日志]
    B --> C[异常捕获]
    C --> D[检查旧实例]
    D --> E[加载Policy]
    E --> F[加载Settings]
    F --> G[升级配置]
    G --> H[初始化语言]
    H --> I[单实例检测]
    I --> J[启动后台任务]
    J --> K[调整线程池]
    K --> L[SplashScreen]
    L --> M[MainWindow]
flowchart TD
    A[Application_Startup] --> B[启动日志]
    B --> C[异常捕获]
    C --> D[检查旧实例]
    D --> E[加载Policy]
    E --> F[加载Settings]
    F --> G[升级配置]
    G --> H[初始化语言]
    H --> I[单实例检测]
    I --> J[启动后台任务]
    J --> K[调整线程池]
    K --> L[SplashScreen]
    L --> M[MainWindow]
flowchart TD
    A[Application_Startup] --> B[启动日志]
    B --> C[异常捕获]
    C --> D[检查旧实例]
    D --> E[加载Policy]
    E --> F[加载Settings]
    F --> G[升级配置]
    G --> H[初始化语言]
    H --> I[单实例检测]
    I --> J[启动后台任务]
    J --> K[调整线程池]
    K --> L[SplashScreen]
    L --> M[MainWindow]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
flowchart TD
    A[Application_Startup] --> B[启动日志]
    B --> C[异常捕获]
    C --> D[检查旧实例]
    D --> E[加载Policy]
    E --> F[加载Settings]
    F --> G[升级配置]
    G --> H[初始化语言]
    H --> I[单实例检测]
    I --> J[启动后台任务]
    J --> K[调整线程池]
    K --> L[SplashScreen]
    L --> M[MainWindow]

Policy从exe所在目录的config.json加载,而LocalSettings从用户家目录\\AppData\\Local\\NETworkManager\\Settings.json文件加载,Settings从用户家目录\\Documents\\NETworkManager\\Settings\\Settings.json文件加载。

Application_Exit

Application_Exit会在应用退出时调用,主要调用了Save,在退出时保存程序设置和配置文件。

Save

Save如果检测到更改,会保存应用程序设置和配置文件数据。

OnSessionEnding

OnSessionEnding会在Windows注销或者Windows关机时被调用。

2. 日志保存在什么位置?

NETworkerManager使用的是log4net日志库。在项目的根目录下有一个log4net.config文件用于配置日志:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<log4net>
    <root>
        <!-- Log level: ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF -->
        <level value="INFO"/>
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
    </root>
    <appender name="console" type="log4net.Appender.ConsoleAppender">
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%date [%thread] %-5level %logger - %message%newline"/>
        </layout>
    </appender>
    <appender name="file" type="log4net.Appender.RollingFileAppender">
        <file value="${LocalAppData}\\NETworkManager\NETworkManager.log"/>
        <appendToFile value="true"/>
        <rollingStyle value="Size"/>
        <maxSizeRollBackups value="5"/>
        <maximumFileSize value="10MB"/>
        <staticLogFileName value="true"/>
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%date [%thread] %-5level %logger - %message%newline"/>
        </layout>
    </appender>
</log4net>

日志保存在${LocalAppData}\\NETworkManager目录中。

3. 仪表盘界面中的IP地址是如何获取的?

如图4所示,仪表盘界面中的IPv4、IPv6以及DNS地址是如何获取的呢?ViewModels/NetworkConnectionWidgetViewModel.cs可以找到答案,调用了CheckConnectionComputerAsyncCheckConnectionRouterAsync以及CheckConnectionInternetAsync分别获取图4所示的计算机、网关/路由、Internet的IPv4、IPv6地址和DNS信息。

NETworkManager代码非常规范,值得我们学习。以CheckConnectionComputerAsync为例,整个函数的逻辑按块进行组织并附上了注释例如// Init variables// Detect local IPv4 address等,读者一看注释便知接下来的代码块功能是什么,大大降低的读者的心智负担。另外,函数、变量的命名也十分清晰,一看便知其作用。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
    /// <summary>
    /// Checks the computer connection.
    /// </summary>
    /// <param name="ct">The cancellation token.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    private Task CheckConnectionComputerAsync(CancellationToken ct)
    {
        return Task.Run(async () =>
        {
            Log.Debug("CheckConnectionComputerAsync - Checking local connection...");

            // Init variables
            IsComputerIPv4Checking = true;
            ComputerIPv4 = "";
            ComputerIPv4State = ConnectionState.None;

            IsComputerIPv6Checking = true;
            ComputerIPv6 = "";
            ComputerIPv6State = ConnectionState.None;

            IsComputerDNSChecking = true;
            ComputerDNS = "";
            ComputerDNSState = ConnectionState.None;

            // Detect local IPv4 address
            Log.Debug("CheckConnectionComputerAsync - Detecting local IPv4 address...");

            var detectedLocalIPv4Address =
                await NetworkInterface.DetectLocalIPAddressBasedOnRoutingAsync(
                    IPAddress.Parse(SettingsManager.Current.Dashboard_PublicIPv4Address));

            if (detectedLocalIPv4Address == null)
            {
                Log.Debug("CheckConnectionComputerAsync - Local IPv4 address detection via routing failed, trying network interfaces...");

                detectedLocalIPv4Address = await NetworkInterface.DetectLocalIPAddressFromNetworkInterfaceAsync(
                    AddressFamily.InterNetwork);
            }

            if (detectedLocalIPv4Address != null)
            {
                Log.Debug("CheckConnectionComputerAsync - Local IPv4 address detected: " + detectedLocalIPv4Address);

                ComputerIPv4 = detectedLocalIPv4Address.ToString();
                ComputerIPv4State = string.IsNullOrEmpty(ComputerIPv4) ? ConnectionState.Critical : ConnectionState.OK;
            }
            else
            {
                Log.Debug("CheckConnectionComputerAsync - Local IPv4 address not detected.");

                ComputerIPv4 = "-/-";
                ComputerIPv4State = ConnectionState.Critical;
            }

            IsComputerIPv4Checking = false;

            if (ct.IsCancellationRequested)
                ct.ThrowIfCancellationRequested();

            // Detect local IPv6 address
            Log.Debug("CheckConnectionComputerAsync - Detecting local IPv6 address...");

            var detectedLocalIPv6Address =
                await NetworkInterface.DetectLocalIPAddressBasedOnRoutingAsync(
                    IPAddress.Parse(SettingsManager.Current.Dashboard_PublicIPv6Address));

            if (detectedLocalIPv6Address == null)
            {
                Log.Debug("CheckConnectionComputerAsync - Local IPv6 address detection via routing failed, trying network interfaces...");

                detectedLocalIPv6Address = await NetworkInterface.DetectLocalIPAddressFromNetworkInterfaceAsync(
                    AddressFamily.InterNetworkV6);
            }

            if (detectedLocalIPv6Address != null)
            {
                Log.Debug("CheckConnectionComputerAsync - Local IPv6 address detected: " + detectedLocalIPv6Address);

                ComputerIPv6 = detectedLocalIPv6Address.ToString();
                ComputerIPv6State = string.IsNullOrEmpty(ComputerIPv6) ? ConnectionState.Critical : ConnectionState.OK;
            }
            else
            {
                Log.Debug("CheckConnectionComputerAsync - Local IPv6 address not detected.");

                ComputerIPv6 = "-/-";
                ComputerIPv6State = ConnectionState.Critical;
            }

            IsComputerIPv6Checking = false;

            if (ct.IsCancellationRequested)
                ct.ThrowIfCancellationRequested();

            // Try to resolve local DNS based on IPv4
            if (ComputerIPv4State == ConnectionState.OK)
            {
                Log.Debug("CheckConnectionComputerAsync - Resolving local DNS based on IPv4...");

                var dnsResult = await DNSClient.GetInstance().ResolvePtrAsync(IPAddress.Parse(ComputerIPv4));

                if (!dnsResult.HasError)
                {
                    Log.Debug("CheckConnectionComputerAsync - Local DNS based on IPv4 resolved: " + dnsResult.Value);

                    ComputerDNS = dnsResult.Value;
                    ComputerDNSState = ConnectionState.OK;
                }
                else
                {
                    Log.Debug("CheckConnectionComputerAsync - Local DNS based on IPv4 not resolved.");
                }
            }
            else
            {
                Log.Debug("CheckConnectionComputerAsync - Local DNS based on IPv4 not resolved due to invalid IPv4 address.");
            }

            // Try to resolve local DNS based on IPv6 if IPv4 failed
            if (string.IsNullOrEmpty(ComputerDNS) && ComputerIPv6State == ConnectionState.OK)
            {
                Log.Debug("CheckConnectionComputerAsync - Resolving local DNS based on IPv6...");

                var dnsResult = await DNSClient.GetInstance().ResolvePtrAsync(IPAddress.Parse(ComputerIPv6));

                if (!dnsResult.HasError)
                {
                    Log.Debug("CheckConnectionComputerAsync - Local DNS based on IPv6 resolved: " + dnsResult.Value);

                    ComputerDNS = dnsResult.Value;
                    ComputerDNSState = ConnectionState.OK;
                }
                else
                {
                    Log.Debug("CheckConnectionComputerAsync - Local DNS based on IPv6 not resolved.");
                }
            }
            else
            {
                Log.Debug("CheckConnectionComputerAsync - Local DNS based on IPv6 not resolved due to IPv4 DNS resolved or invalid IPv6 address");
            }

            if (string.IsNullOrEmpty(ComputerDNS))
            {
                ComputerDNS = "-/-";
                ComputerDNSState = ConnectionState.Critical;
            }

            IsComputerDNSChecking = false;

            Log.Debug("CheckConnectionComputerAsync - Local connection check completed.");
        }, ct);
    }

为获取IPv4地址,准备了两种方式:一种是DetectLocalIPAddressBasedOnRoutingAsync创建UDP套接字确定本机IP,另一种是DetectLocalIPAddressFromNetworkInterfaceAsync配合Microsoft.PowerShell.SDK,用C#实现了类似Windows ipconfig /all + PowerShell Get-NetConnectionProfile的效果。

4. 网络接口选项中的接口带宽是如何测出来的?

图5 接口带宽界面
图5 接口带宽界面

如图5所示,网络接口选项中包含了一个接口带宽界面,其中的速度是如何得到的?NETworkManager.Models/Network/BandwidthMeter.csBandwidthMeterUpdate方法可以找到答案。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
IPInterfaceStatistics stats;

try
{
    // IPStatistics covers both IPv4 and IPv6 traffic on the interface.
    stats = _networkInterface.GetIPStatistics();
}
catch (NetworkInformationException)
{
    // Transient failure (e.g. adapter going down) - skip this tick.
    return;
}

var totalBytesSent = stats.BytesSent;
var totalBytesReceived = stats.BytesReceived;

System.Net.NetworkInformation.NetworkInterface类的GetIPStatistics方法可以获取到一些统计信息,其中就包括已发送的字节数和已接收的字节数。为了得到速度,可以计算在t时间段内发送字节数和接收字节数的差异与t的比值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var elapsedSeconds = _stopwatch.Elapsed.TotalSeconds;
_stopwatch.Restart();

// T时刻发送的字节数是_previousBytesSent
// T+elapsedSeconds时刻发送的字节数是totalBytesSent
var deltaSent = Math.Max(0, totalBytesSent - _previousBytesSent);
// T时刻接收的字节数是_previousBytesReceived
// T+elapsedSeconds时刻发送的字节数是totalBytesReceived
var deltaReceived = Math.Max(0, totalBytesReceived - _previousBytesReceived);

var byteSentSpeed = elapsedSeconds > 0 ? (long)(deltaSent / elapsedSeconds) : 0;
var byteReceivedSpeed = elapsedSeconds > 0 ? (long)(deltaReceived / elapsedSeconds) : 0;

BandwidthMeter中有一个_timer定时器,默认每隔1秒就会调用1次Update函数。该类还定义了一个UpdateSpeed事件,而NETworkManager/ViewModels/NetworkInterfaceViewModel.csNetworkInterfaceViewModel订阅了该事件:

1
_bandwidthMeter.UpdateSpeed += BandwidthMeter_UpdateSpeed;

这意味着每隔1秒BandwidthMeter_UpdateSpeed方法就会被调用1次。BandwidthMeter_UpdateSpeed主要包含了一些图表数据更新的逻辑,这就是为什么我们可以看到图表的曲线一直在刷新变化。这里的图表看起来比较精美,用的是LiveCharts2,一个现代化的 .NET 数据可视化图表库。

5. NETworkManager是如何管理众多功能页面的?

程序左侧的菜单使用了ListView组件:

NETworkManager/MainWindow.xaml
1
2
3
4
5
6
7
8
<ListView x:Name="ListViewApplication" ItemsSource="{Binding Applications}"
          SelectedItem="{Binding SelectedApplication, Mode=TwoWay}"
          SelectionMode="Single"
          BorderThickness="0"
          dd:DragDrop.IsDragSource="True"
          dd:DragDrop.IsDropTarget="True"> 
          ...... 
</ListView>

该组件绑定了SelectedApplication,其定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
   public ApplicationInfo SelectedApplication
   {
       get;
       set
       {
           // Do not change the application view if the application view is loading
           if (_isApplicationViewLoading)
               return;

           // Should only be null if we try to re-set the selected application via search
           if (value == null && !_applicationViewIsEmpty)
               return;

           // Don't update the application view if the application is the same
           if (Equals(value, field))
               return;

           if (value != null && !_applicationViewIsEmpty)
           {
               // Hide the old application view
               if (field != null)
                   OnApplicationViewHide(field.Name);

               // Show the new application view
               OnApplicationViewVisible(value.Name);

               // Store the last selected application name
               ConfigurationManager.Current.CurrentApplication = value.Name;
           }

           field = value;
           OnPropertyChanged();
       }
   }

页面的显示由OnApplicationViewVisible(value.Name)决定,OnApplicationViewVisible会根据点击菜单的不同,将ContentControlApplication.Content的值设置为对应的页面。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  private void OnApplicationViewVisible(ApplicationName name)
  {
      switch (name)
      {
          case ApplicationName.Dashboard:
              if (_dashboardView == null)
                  _dashboardView = new DashboardView();
              else
                  _dashboardView.OnViewVisible();

              ContentControlApplication.Content = _dashboardView;
              break;
          case ApplicationName.NetworkInterface:
              if (_networkInterfaceView == null)
                  _networkInterfaceView = new NetworkInterfaceView();
              else
                  _networkInterfaceView.OnViewVisible();

              ContentControlApplication.Content = _networkInterfaceView;
              break;
          // ......
      }
  }

ContentControlApplicationNETworkManager/MainWindow.xaml中定义的一个ContentControl内容容器控件而已(常用于页面切换):

NETworkManager/MainWindow.xaml
1
2
3
4
<asf:AirspacePanel Grid.Column="1" Grid.Row="0"
                    FixAirspace="{Binding Source={x:Static settings:ConfigurationManager.Current}, Path=FixAirspace}">
    <ContentControl x:Name="ContentControlApplication" />
</asf:AirspacePanel>

6. 语言切换,如何做到不用修改代码?

NETworkManager/MainWindow.xaml.csMainWindow构造函数对WPF 控件的语言环境进行了设置:

1
2
3
4
// Language metadata
LanguageProperty.OverrideMetadata(typeof(FrameworkElement),
    new FrameworkPropertyMetadata(
        XmlLanguage.GetLanguage(LocalizationManager.GetInstance().Culture.IetfLanguageTag)));

会影响到字体、文本布局等。而NETworkManager/App.xaml.cs中的Application_Startup方法对字符串资源进行选择:

1
2
3
4
// Initialize localization
var localizationManager = LocalizationManager.GetInstance(SettingsManager.Current.Localization_CultureCode);

Strings.Culture = localizationManager.Culture;

字符串资源可在xaml文件中使用。

图6 使用字符串资源
图6 使用字符串资源

7. 软件如何实现自动更新?

MainWindow.xaml.cs中定义了一个CheckForUpdates函数用于检测软件是否要更新:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private void CheckForUpdates()
{
    var updater = new Updater();

    updater.UpdateAvailable += Updater_UpdateAvailable;

    updater.CheckOnGitHub(Properties.Resources.NETworkManager_GitHub_User,
        Properties.Resources.NETworkManager_GitHub_Repo, AssemblyManager.Current.Version,
        SettingsManager.Current.Update_CheckForPreReleases);
}

主要逻辑就是比较当前版本与Github Release最新版本,如果更低则说明要更新,具体逻辑如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    public void CheckOnGitHub(string userName, string projectName, Version currentVersion, bool includePreRelease)
    {
        Task.Run(() =>
        {
            try
            {
                Log.Info("Checking for new version on GitHub...");

                // Create GitHub client
                var client = new GitHubClient(new ProductHeaderValue(userName + "_" + projectName));

                // Get latest or pre-release version
                var release = includePreRelease
                    ? client.Repository.Release.GetAll(userName, projectName).Result[0]
                    : client.Repository.Release.GetLatest(userName, projectName).Result;

                // Compare versions (tag=2021.2.15.0, version=2021.2.15.0)
                if (new Version(release.TagName) > currentVersion)
                {
                    Log.Info($"Version \"{release.TagName}\" is available!");
                    OnUpdateAvailable(new UpdateAvailableArgs(release));
                }
                else
                {
                    Log.Info("No newer version found!");
                    OnNoUpdateAvailable();
                }
            }
            catch (Exception ex)
            {
                Log.Error("Error while checking for new version on GitHub!", ex);
                OnError();
            }
        });
    }

为了获取Github上最新Release版本,使用了一个名为Octokit的三方库。

番外

.editorconfig是什么?

.editorconfigEditorConfig的配置文件,能够让不同编辑器保持编辑风格一致。比如:用四个空格缩进、用utf-8编码等等。

[CallerMemberName]是什么?

NETworkManager.Utilities/PropertyChangedBase.csOnPropertyChanged方法使用了[CallerMemberName],这是什么?

1
2
3
4
5
6
7
8
9
public abstract class PropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

[CallerMemberName]是C#中的一个调用方信息属性(Caller Information Attribute),它可以让方法自动获取调用者的成员名称(方法名、属性名、构造函数名等),而不需要手动传递字符串。它位于:

1
using System.Runtime.CompilerServices;

例如:

1
2
3
4
5
6
7
8
    public static void Log(string message, [CallerMemberName]string memberName = "")
    {
        Console.WriteLine($"{memberName}: {message}");
    }
    static void Main(string[] args)
    {
        Log("Hello World");
    }

输出:

1
Main: Hello World

最经典的用途就是像PropertyChangedBase一样,实现属性通知。


相关内容

请作者喝杯咖啡!
AndyFree96 支付宝支付宝
AndyFree96 微信微信