CameraBase.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Diagnostics;
  5. using System.Linq;
  6. using System.Runtime.CompilerServices;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using TeamAAS.Camera.Enums;
  10. using TeamAAS.Camera.Images;
  11. using TeamAAS.Camera.Interfaces;
  12. namespace TeamAAS.Camera.Cameras
  13. {
  14. /// <summary>
  15. /// 相机基类:封装所有品牌相机的通用功能。
  16. /// 所有 SDK 操作均通过 _cameraLock(可重入读写锁)保护,防止多线程并发访问同一相机。
  17. /// </summary>
  18. public abstract class CameraBase : ICamera, INotifyPropertyChanged
  19. {
  20. #region 字段
  21. protected Thread m_hReceiveThread;
  22. protected bool _isHaveCamera = false;
  23. /// <summary>
  24. /// 相机操作互斥锁(可重入)。
  25. /// 所有与 SDK 的交互(开/关/取相/参数设置)必须在写锁内执行。
  26. /// </summary>
  27. private readonly ReaderWriterLockSlim _cameraLock =
  28. new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
  29. #endregion
  30. #region 基本信息
  31. private string _name;
  32. [Category("I.基本信息"), DisplayName("1.名称"), Description("自定义相机名称")]
  33. public string Name
  34. {
  35. get => _name;
  36. set => SetProperty(ref _name, value);
  37. }
  38. [Category("I.基本信息"), DisplayName("2.曝光时间(μs)"), Description("曝光时间,单位微秒")]
  39. public float ExposureTime
  40. {
  41. get => ReadParameter(GetExposureTime, ref _exposureTime);
  42. set => SetImageParameter(
  43. () => SetExposureTime(value),
  44. () => { _exposureTime = value; _hasExposure = true; },
  45. value, $"曝光时间 {value}μs", nameof(ExposureTime));
  46. }
  47. [Category("I.基本信息"), DisplayName("3.增益(亮度)"), Description("增益越高画面越亮,噪点也随之增大")]
  48. public float Gain
  49. {
  50. get => ReadParameter(GetGain, ref _gain);
  51. set => SetImageParameter(
  52. () => SetGain(value),
  53. () => { _gain = value; _hasGain = true; },
  54. value, $"增益 {value}", nameof(Gain));
  55. }
  56. [Category("I.基本信息"), DisplayName("4.Gamma"), Description("Gamma 校正系数,默认 1.0 表示不校正")]
  57. public float Gamma
  58. {
  59. get => ReadParameter(GetGamma, ref _gamma);
  60. set => SetImageParameter(
  61. () => SetGamma(value),
  62. () => { UpdateGammaSnapshot(value); _hasGamma = true; },
  63. value, $"Gamma {value}", nameof(Gamma));
  64. }
  65. #endregion
  66. #region 触发模式
  67. /// <summary>所有相机都具备的最小能力集:跟随相机(不下发)+ 连续采集。</summary>
  68. protected static readonly IReadOnlyList<CameraTriggerMode> ContinuousOnlyTriggerModes =
  69. new[] { CameraTriggerMode.跟随相机, CameraTriggerMode.连续采集 };
  70. /// <summary>全功能触发模式集,供支持软/硬触发的工业相机直接复用。</summary>
  71. protected static readonly IReadOnlyList<CameraTriggerMode> FullTriggerModes =
  72. new[]
  73. {
  74. CameraTriggerMode.跟随相机,
  75. CameraTriggerMode.连续采集,
  76. CameraTriggerMode.软触发,
  77. CameraTriggerMode.硬触发上升沿,
  78. CameraTriggerMode.硬触发下降沿,
  79. };
  80. /// <summary>
  81. /// 本相机支持的触发模式(默认仅“跟随相机/连续采集”)。
  82. /// 子类按自家 SDK 能力重写:支持软/硬触发的直接返回 <see cref="FullTriggerModes"/>;
  83. /// 无触发能力的(如 UVC 网络摄像头)保持默认即可——上层下拉列表会随之增减。
  84. /// </summary>
  85. [Browsable(false)]
  86. public virtual IReadOnlyList<CameraTriggerMode> SupportedTriggerModes => ContinuousOnlyTriggerModes;
  87. /// <summary>
  88. /// 相机连接后的默认触发模式。子类若原先在 OpenDevice 里硬编码了触发设置,
  89. /// 必须在此返回等价模式,以保证既有行为不变(如 Basler/OPT 原为软触发)。
  90. /// </summary>
  91. protected virtual CameraTriggerMode DefaultTriggerMode => CameraTriggerMode.跟随相机;
  92. private CameraTriggerMode _triggerMode = CameraTriggerMode.跟随相机;
  93. /// <summary>当前生效的触发模式</summary>
  94. [Browsable(false)]
  95. public CameraTriggerMode TriggerMode
  96. {
  97. get => _triggerMode;
  98. protected set => SetProperty(ref _triggerMode, value);
  99. }
  100. /// <summary>当前是否软触发:子类 Grab 据此决定是否发 TriggerSoftware 命令</summary>
  101. [Browsable(false)]
  102. public bool IsSoftwareTrigger => _triggerMode == CameraTriggerMode.软触发;
  103. /// <summary>当前是否外部硬触发:取相需等待外部信号,无信号会超时</summary>
  104. [Browsable(false)]
  105. public bool IsExternalTrigger =>
  106. _triggerMode == CameraTriggerMode.硬触发上升沿 ||
  107. _triggerMode == CameraTriggerMode.硬触发下降沿;
  108. /// <summary>该相机是否支持指定触发模式(“跟随相机”恒支持,因为它不下发任何设置)</summary>
  109. public bool SupportsTriggerMode(CameraTriggerMode mode)
  110. => mode == CameraTriggerMode.跟随相机 ||
  111. (SupportedTriggerModes != null && SupportedTriggerModes.Contains(mode));
  112. /// <summary>
  113. /// 设置触发模式:先按 <see cref="SupportedTriggerModes"/> 校验能力,再交子类下发硬件。
  114. /// 无论下发成败都落地期望值快照(相机未连接时由 OpenDevice 补下发)。
  115. /// </summary>
  116. public virtual bool SetTriggerMode(CameraTriggerMode mode)
  117. {
  118. // “跟随相机”= 不接管触发设置,保持相机当前生效模式。
  119. // 必须原封不动返回:若把快照改成“跟随相机”,软触发相机会停发 TriggerSoftware
  120. // 而硬件仍在 TriggerMode=On 等触发,导致取相一直超时卡死。
  121. if (mode == CameraTriggerMode.跟随相机)
  122. {
  123. ErrorMessage = null;
  124. return true;
  125. }
  126. if (!SupportsTriggerMode(mode))
  127. {
  128. ErrorMessage = $"{Name} 不支持触发模式「{mode}」(支持:{string.Join("、", SupportedTriggerModes ?? ContinuousOnlyTriggerModes)})";
  129. return false;
  130. }
  131. try
  132. {
  133. bool ok = ApplyTriggerMode(mode);
  134. ErrorMessage = ok ? null : $"设置触发模式「{mode}」失败(若相机未连接,连接后将自动应用)";
  135. return ok;
  136. }
  137. catch (Exception ex)
  138. {
  139. ErrorMessage = $"设置触发模式异常: {ex.Message}";
  140. return false;
  141. }
  142. finally
  143. {
  144. TriggerMode = mode;
  145. }
  146. }
  147. /// <summary>
  148. /// 子类实现:把触发模式下发到 SDK。基类默认空实现(仅记录快照),适用于无触发能力的相机。
  149. /// 若 SDK 调用需要互斥,实现内自行用 <see cref="WriteLockScope"/>(锁可重入)。
  150. /// </summary>
  151. protected virtual bool ApplyTriggerMode(CameraTriggerMode mode) => true;
  152. #endregion
  153. #region 公共属性
  154. public abstract string CameraBrand { get; }
  155. [Browsable(false)]
  156. public Guid ID { get; protected set; }
  157. [Browsable(false)]
  158. public int Index { get; set; }
  159. [Category("杂项"), DisplayName("1.厂商"), ReadOnly(true)]
  160. public string ManufacturerName { get; protected set; }
  161. [Category("杂项"), DisplayName("2.型号"), ReadOnly(true)]
  162. public string ModelName { get; protected set; }
  163. [Category("杂项"), DisplayName("3.序列号"), ReadOnly(true)]
  164. public string SerialNumber { get; protected set; }
  165. [Category("杂项"), DisplayName("4.IP地址"), ReadOnly(true)]
  166. public string CameraIp { get; protected set; }
  167. private bool _isConnected;
  168. [Category("杂项"), DisplayName("5.连接状态"), ReadOnly(true)]
  169. public bool IsConnected
  170. {
  171. get { return _isConnected; }
  172. protected set { SetProperty(ref _isConnected, value); }
  173. }
  174. #endregion
  175. #region Orthor
  176. [Browsable(false)]
  177. public bool IsFindByIp { get; protected set; }
  178. private bool _isGrabbing;
  179. [Browsable(false)]
  180. public bool IsGrabbing
  181. {
  182. get { return _isGrabbing; }
  183. protected set { SetProperty(ref _isGrabbing, value); }
  184. }
  185. private GenImage _image;
  186. [Browsable(false)]
  187. public GenImage Image
  188. {
  189. get { return _image; }
  190. protected set { SetProperty(ref _image, value); }
  191. }
  192. [Browsable(false)]
  193. public CameraType CameraType { get; protected set; }
  194. [Browsable(false)]
  195. public UInt32 ImageWidth { get; protected set; }
  196. [Browsable(false)]
  197. public UInt32 ImageHeight { get; protected set; }
  198. [Browsable(false)]
  199. public TimeSpan TotalTime { get; protected set; }
  200. [Browsable(false)]
  201. public string ErrorMessage { get; protected set; }
  202. #endregion
  203. #region 事件
  204. /// <summary>
  205. /// 取相
  206. /// </summary>
  207. public event Action<GenImage, TimeSpan, string> ImageCallbackEvent;
  208. public event Action<Guid, bool> CameraConnectChangedEvent;
  209. #endregion
  210. #region 构造函数
  211. protected CameraBase(Guid id, int index, string name, string serialNumber, string cameraIp, bool isFindByIp)
  212. {
  213. ID = id;
  214. Name = name;
  215. Index = index;
  216. CameraIp = cameraIp;
  217. SerialNumber = serialNumber;
  218. IsFindByIp = isFindByIp;
  219. IsConnected = false;
  220. // 按子类声明的默认触发模式初始化快照(等价于原来 OpenDevice 里的硬编码设置)
  221. _triggerMode = DefaultTriggerMode;
  222. }
  223. #endregion
  224. #region 线程同步辅助
  225. /// <summary>
  226. /// 获取写锁作用域 — 用于所有 SDK 写操作(开/关/取相/参数设置)。
  227. /// 使用方式:using (WriteLockScope()) { ... }
  228. /// </summary>
  229. protected IDisposable WriteLockScope()
  230. {
  231. _cameraLock.EnterWriteLock();
  232. return new WriteLockDisposable(_cameraLock);
  233. }
  234. /// <summary>
  235. /// 获取读锁作用域 — 用于只读 SDK 操作(获取参数值)。
  236. /// </summary>
  237. protected IDisposable ReadLockScope()
  238. {
  239. _cameraLock.EnterReadLock();
  240. return new ReadLockDisposable(_cameraLock);
  241. }
  242. /// <summary>
  243. /// 获取写锁(无 using 模式时使用,需手动调用 WriteUnlock)。
  244. /// </summary>
  245. protected void WriteLock()
  246. {
  247. _cameraLock.EnterWriteLock();
  248. }
  249. /// <summary>
  250. /// 释放写锁。
  251. /// </summary>
  252. protected void WriteUnlock()
  253. {
  254. _cameraLock.ExitWriteLock();
  255. }
  256. /// <summary>
  257. /// 获取读锁(无 using 模式时使用,需手动调用 ReadUnlock)。
  258. /// </summary>
  259. protected void ReadLock()
  260. {
  261. _cameraLock.EnterReadLock();
  262. }
  263. /// <summary>
  264. /// 释放读锁。
  265. /// </summary>
  266. protected void ReadUnlock()
  267. {
  268. _cameraLock.ExitReadLock();
  269. }
  270. private sealed class WriteLockDisposable : IDisposable
  271. {
  272. private readonly ReaderWriterLockSlim _lock;
  273. public WriteLockDisposable(ReaderWriterLockSlim rwLock) { _lock = rwLock; }
  274. public void Dispose() { _lock.ExitWriteLock(); }
  275. }
  276. private sealed class ReadLockDisposable : IDisposable
  277. {
  278. private readonly ReaderWriterLockSlim _lock;
  279. public ReadLockDisposable(ReaderWriterLockSlim rwLock) { _lock = rwLock; }
  280. public void Dispose() { _lock.ExitReadLock(); }
  281. }
  282. #endregion
  283. #region 通用实现(不需要子类重写)
  284. private int _readingParameters;
  285. private float _exposureTime;
  286. private float _gain;
  287. private float _gamma = 1.0f;
  288. // 用户是否显式设置过对应参数(含从配置加载)。只有设置过才会在连接/采集时自动下发,
  289. // 避免把默认值(曝光 0、增益 0)强行推给硬件。
  290. private bool _hasExposure;
  291. private bool _hasGain;
  292. private bool _hasGamma;
  293. /// <summary>
  294. /// 参数写入统一路径:先尝试下发硬件(无论成败),再把期望值落地到本地快照。
  295. /// 失败/异常只记 ErrorMessage 并返回——相机未连接时用户设置的值不会丢,
  296. /// 连接后会经 TryApplyImageParameters 自动应用。
  297. /// </summary>
  298. private void SetImageParameter(Func<bool> apply, Action updateSnapshot, float value, string label, string notifyProperty)
  299. {
  300. try
  301. {
  302. bool ok = apply();
  303. ErrorMessage = ok ? null : $"设置{label}失败(若相机未连接,连接后将自动应用)";
  304. }
  305. catch (Exception ex)
  306. {
  307. ErrorMessage = $"设置{label}异常: {ex.Message}";
  308. }
  309. finally
  310. {
  311. updateSnapshot();
  312. }
  313. RaisePropertyChanged(notifyProperty);
  314. }
  315. /// <summary>
  316. /// 把本地保存的期望参数值推送到硬件(幂等,可重复调用)。
  317. /// 触发时机:连接成功(OpenDeviceAsync)、开始采集(StartGrabbing)、配置初始化后首连。
  318. /// 仅推送用户显式设置过的参数,避免把默认值强写到硬件。
  319. /// </summary>
  320. public void TryApplyImageParameters()
  321. {
  322. if (!IsConnected) return;
  323. // 自动曝光是相机品牌特有(UVC)的运行时开关,不在这里处理;
  324. // 各品牌的 SetExposureTime/SetGain 会自行判断是否处于自动模式,并据此忽略手动写入。
  325. if (_hasExposure) { try { SetExposureTime(_exposureTime); } catch { } }
  326. if (_hasGain) { try { SetGain(_gain); } catch { } }
  327. if (_hasGamma) { try { SetGamma(_gamma); } catch { } }
  328. }
  329. /// <summary>
  330. /// 从 SDK 读取参数值并同步到快照。
  331. /// 重入防护:子类 GetXxx 的兜底若再读本属性(_readingParameters > 0),直接返回快照阻断递归;
  332. /// SDK 异常(未连接等)同样回退快照。计数为实例级、非原子 —— 本属性仅由 UI/业务线程按序访问。
  333. /// </summary>
  334. private float ReadParameter(Func<float> read, ref float snapshot)
  335. {
  336. if (_readingParameters > 0) return snapshot;
  337. _readingParameters++;
  338. try { snapshot = read(); return snapshot; }
  339. catch { return snapshot; }
  340. finally { _readingParameters--; }
  341. }
  342. /// <summary>
  343. /// 仅更新本地 Gamma 快照并通知 UI(不写硬件),供基类默认实现与子类兜底路径使用。
  344. /// 注意:子类内部请勿写 "Gamma = x"——属性 setter 会再次进入 SetGamma 造成无限递归。
  345. /// </summary>
  346. protected void UpdateGammaSnapshot(float gamma)
  347. {
  348. SetProperty(ref _gamma, gamma);
  349. }
  350. /// <summary>
  351. /// 打开相机异步(通用实现,包装 OpenDevice)。
  352. /// 打开成功后自动把本地保存的参数(曝光/增益/Gamma)下发到硬件。
  353. /// </summary>
  354. public Task<bool> OpenDeviceAsync()
  355. {
  356. return Task.Run(() =>
  357. {
  358. bool opened = OpenDevice();
  359. if (opened) TryApplyImageParameters();
  360. return opened;
  361. });
  362. }
  363. /// <summary>
  364. /// 开始实时采集图像(通用线程管理)。启动前把待应用的参数快照推送到硬件。
  365. /// </summary>
  366. public virtual void StartGrabbing()
  367. {
  368. if (IsConnected) TryApplyImageParameters();
  369. using (WriteLockScope())
  370. {
  371. if (IsGrabbing) return;
  372. IsGrabbing = true;
  373. m_hReceiveThread = new Thread(GetStreamThreadProc) { IsBackground = true };
  374. m_hReceiveThread.Start();
  375. }
  376. }
  377. /// <summary>
  378. /// 停止实时采集图像(通用线程管理)
  379. /// </summary>
  380. public virtual void StopGrabbing()
  381. {
  382. using (WriteLockScope())
  383. {
  384. try
  385. {
  386. IsGrabbing = false;
  387. if (m_hReceiveThread != null && m_hReceiveThread.IsAlive)
  388. {
  389. m_hReceiveThread.Abort();
  390. m_hReceiveThread = null;
  391. }
  392. }
  393. catch { }
  394. }
  395. }
  396. /// <summary>
  397. /// 实时采集线程过程(通用实现)
  398. /// </summary>
  399. protected virtual void GetStreamThreadProc()
  400. {
  401. while (IsGrabbing)
  402. {
  403. try
  404. {
  405. Grab();
  406. ImageCallbackEvent?.Invoke(Image, TotalTime, ErrorMessage);
  407. }
  408. catch { }
  409. Thread.Sleep(10);
  410. }
  411. IsGrabbing = false;
  412. }
  413. /// <summary>
  414. /// 批量设置相机参数(子类可重写以支持更多参数)
  415. /// </summary>
  416. public virtual void ApplyParameters(Dictionary<string, object> parameters)
  417. {
  418. if (parameters == null) return;
  419. using (WriteLockScope())
  420. {
  421. foreach (var kv in parameters)
  422. {
  423. try
  424. {
  425. switch (kv.Key.ToLower())
  426. {
  427. case "exposure":
  428. case "exposuretime":
  429. if (kv.Value is float f1) SetExposureTime(f1);
  430. else if (kv.Value is double d1) SetExposureTime((float)d1);
  431. break;
  432. case "gain":
  433. if (kv.Value is float f2) SetGain(f2);
  434. else if (kv.Value is double d2) SetGain((float)d2);
  435. break;
  436. case "gamma":
  437. if (kv.Value is float f3) SetGamma(f3);
  438. else if (kv.Value is double d3) SetGamma((float)d3);
  439. break;
  440. }
  441. }
  442. catch { }
  443. }
  444. }
  445. }
  446. /// <summary>
  447. /// 获取当前相机参数快照
  448. /// </summary>
  449. public virtual Dictionary<string, object> GetParameters()
  450. {
  451. var dict = new Dictionary<string, object>();
  452. using (ReadLockScope())
  453. {
  454. try { dict["ExposureTime"] = GetExposureTime(); } catch { }
  455. try { dict["Gain"] = GetGain(); } catch { }
  456. try { dict["Gamma"] = GetGamma(); } catch { }
  457. }
  458. return dict;
  459. }
  460. #endregion
  461. #region 保护方法
  462. /// <summary>
  463. /// 触发连接状态变更事件
  464. /// </summary>
  465. protected void OnConnectChanged(bool connected)
  466. {
  467. CameraConnectChangedEvent?.Invoke(ID, connected);
  468. }
  469. /// <summary>
  470. /// 设置错误信息并返回 null
  471. /// </summary>
  472. protected GenImage SetError(string message)
  473. {
  474. ErrorMessage = message;
  475. return null;
  476. }
  477. /// <summary>
  478. /// 对图像应用当前 Gamma 校正(软件后处理)。
  479. /// 子类应在 Grab() 成功获取图像后调用此方法。
  480. /// </summary>
  481. protected void ApplyGammaToImage(GenImage image)
  482. {
  483. if (image == null) return;
  484. if (Math.Abs(_gamma - 1.0f) < 0.001f) return;
  485. image.ApplyGamma(_gamma);
  486. image.SetMetadata("Gamma", _gamma);
  487. }
  488. /// <summary>
  489. /// 终处理:对图像应用 Gamma、写入 Metadata,返回处理后的图像。
  490. /// 子类应在 Grab() 成功获取图像后调用。
  491. /// </summary>
  492. protected GenImage FinalizeImage(GenImage image)
  493. {
  494. if (image == null) return null;
  495. ApplyGammaToImage(image);
  496. image.SetMetadata("CameraName", Name);
  497. image.SetMetadata("Timestamp", DateTime.Now);
  498. return image;
  499. }
  500. #endregion
  501. #region 抽象方法(子类必须实现,必须在方法体开头使用 WriteLockScope/ReadLockScope)
  502. /// <summary>打开相机</summary>
  503. public abstract bool OpenDevice();
  504. /// <summary>关闭相机</summary>
  505. public abstract void CloseDevice();
  506. /// <summary>采集单张图片,返回通用 GenImage</summary>
  507. public abstract GenImage Grab();
  508. /// <summary>设置曝光时间</summary>
  509. public abstract bool SetExposureTime(float exposureTime);
  510. /// <summary>获取曝光时间</summary>
  511. public abstract float GetExposureTime();
  512. /// <summary>设置增益</summary>
  513. public abstract bool SetGain(float gain);
  514. /// <summary>获取增益</summary>
  515. public abstract float GetGain();
  516. /// <summary>
  517. /// 设置 Gamma 值(默认实现为软件后处理,子类可重写以支持硬件 Gamma)。
  518. /// 实现内部只允许通过 <see cref="UpdateGammaSnapshot"/> 更新快照,禁止给 Gamma 属性赋值(会递归)。
  519. /// </summary>
  520. public virtual bool SetGamma(float gamma)
  521. {
  522. UpdateGammaSnapshot(gamma);
  523. return true;
  524. }
  525. /// <summary>
  526. /// 获取当前 Gamma 值(直接返回本地快照字段,不经过 Gamma 属性——避免与属性 getter 互相递归)
  527. /// </summary>
  528. public virtual float GetGamma() => _gamma;
  529. #endregion
  530. #region INotifyPropertyChanged
  531. public event PropertyChangedEventHandler PropertyChanged;
  532. protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
  533. {
  534. if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
  535. storage = value;
  536. RaisePropertyChanged(propertyName);
  537. return true;
  538. }
  539. protected virtual bool SetProperty<T>(ref T storage, T value, Action onChanged, [CallerMemberName] string propertyName = null)
  540. {
  541. if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
  542. storage = value;
  543. onChanged?.Invoke();
  544. RaisePropertyChanged(propertyName);
  545. return true;
  546. }
  547. protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
  548. {
  549. OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
  550. }
  551. protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
  552. {
  553. PropertyChanged?.Invoke(this, args);
  554. }
  555. #endregion
  556. }
  557. }