در صورتی که خط
import * as _ from "lodash";
import * as _ from "lodash";
آیا به این نتیجه رسیدید که اصل DRY را نقض کردهایم؟ بله همین طور است. تکرار کلاسهای css مربوط به بوت استرپ، تکرار هلپرهای توکار ASP.NET MVC بارها و بارها، خوانایی کد را پایین میارود و در برخی موارد هم خسته کننده خواهد بود. اگر با مباحث مربوط به EditorTemplateها قبلا آشنا شده باشید، خیلی سریع عنوان خواهید کرد که بهتر است از این امکان بهره برد؛ بله درست است. برای این منظور در مسیر Views/Shared/EditorTemplates، فایل cshtml. همنام با نوع داده مد نظر را ایجاد میکنیم.
String.cshtml
@model string @Html.TextBox("",ViewData.TemplateInfo.FormattedModelValue, new { @class="form-control",placeholder=ViewData.ModelMetadata.Watermark})
Enum.cshtml
@model Enum @Html.EnumDropDownListFor(m => Model, new { @class = "form-control" })
حال دوباره به نتیجه حاصل از تغییرات اعمال شده توجه کنید:
این نتیجه امیدوار کننده است ولی بازهم یکسری از کدها بی دلیل تکرار شدهاند. هلپرهای زیر نیز میتوانند در کاهش کدها به کمک ما برسند :
public static class BootstrapHelpers { public static IHtmlString BootstrapLabelFor<TModel,TProp>( this HtmlHelper<TModel> helper, Expression<Func<TModel,TProp>> property) { return helper.LabelFor(property, new { @class = "col-md-2 control-label" }); } public static IHtmlString BootstrapLabel( this HtmlHelper helper, string propertyName) { return helper.Label(propertyName, new { @class = "col-md-2 control-label" }); } }
از کلاس بالا برای عدم تکرار کلاسهای بوت استرپ مربوط به Label، استفاده میشود .
حال دوباره نتیجه را مشاهده کنید:
خیلی عالی؛ توانستیم از تکرار یکسری از کلاسهای بوت استرپ خلاص شویم. اما در ادامه با استفاده از یک Object Template به عنوان EditorTemplate برای نوع دادههای Complex، کار را تمام خواهیم کرد.
EditorTemplateهای تعریف شده در بالا، صرفا برای نوع دادههای خاصی مورد استفاده قرار خواهند گرفت؛ ولی پیاده سازی یک EditorTemplate جنریک که حتی از ویومدلهای موجود در پروژه نیز پشتیابی کند، به شکل زیر خواهد بود.
Object.cshtml
@model dynamic @foreach (var prop in ViewData.ModelMetadata.Properties .Where(p => p.ShowForEdit)) { if (prop.TemplateHint == "HiddenInput") { @Html.Hidden(prop.PropertyName) } else { <div class="form-group"> @Html.BootstrapLabel(prop.PropertyName) <div class="col-md-10"> @Html.Editor(prop.PropertyName) @Html.ValidationMessage(prop.PropertyName) </div> </div> } }
با استفاده از ViewData.ModelMetadata میتوان به خصوصیات مدل مربوط به ویو دسترسی پیدا کرد که در بالا با استفاده از همین خصوصیت به تمام پراپرتیهای مدل دسترسی پیدا کرده و مقداری کد تکراری باقی مانده را هم در اینجا کپسوله کردیم.
حال کافی است به شکل زیر عمل کنیم:
تا اینجای کار ساخت کامپوننتها را با React.createClass که تفاوتی با توسعه (ارث بری) از کلاس React.Component ندارد، انجام دادهایم. اما ساخت کامپوننتها به صورت یک تابع هم مزیتهایی را دارد. اول از همه باید بدانیم که ساخت کامپوننت توسط تابع، بدون وضعیت خواهد بود که به آن Stateless میگویند. به دلیل نداشتن وضعیت، کامپوننتهای تابعی را کمی بهتر میشود برای استفاده مجدد به کار برد. در کامپوننتهای غیر تابعی که Stateful هستند به دلیل احتمال وابستگی وضعیت کامپوننت به خارج از کلاس، مانند مثال قسمت چهارم که کامپوننت در انتظار کلیک یک دکمه خاص توسط کاربر بود، مدیریت استفاده مجدد ازکامپوننت چالش برانگیز خواهد شد.
برای مدیریت بهتر کامپوننتها جهت استفاده مجدد از آنها بهتر است ورودیهای کامپوننت را اعتبارسنجی کنیم. این ورودیها چه برای استفاده داخلی کامپوننت، یا جهت مشخص کردن وضعیت آن، بر رفتار کامپوننت تاثیر زیادی دارند. React مجموعهای از اعتبار سنجیها را دارد که میشود به کامپوننت اضافه کرد. باید توجه داشته باشیم که پیامهای خطای این اعتبارسنجیها فقط در حالت Development Mode قابل استفاده هستند. به زبان ساده اگر از react.min.js استفاده کنید، پیامهای خطا را نخواهید دید. باید فایلها را به نوع react.js تبدیل کنید. اعتبارسنجی React در زمان توسعه و برای توسعه دهندگان استفاده میشود.
مثال نوشیدنیها در قسمت چهارم میتوانست نام نوشیدنی و قیمت آن را نمایش دهد و همچنین میتوانستیم به لیست نوشیدنیها، موردی را اضافه کنیم. اگر ورودی قیمت، اعتبارسنجی نشود، میتوان رشتهای را بجای عدد به عنوان قیمت به کامپوننت ارسال کرد. نحوه اعتبارسنجی قیمت نوشیدنیها به صورت زیر است.
const MenuItem = props => ( <li className="list-group-item"> <span className="badge">{props.price}</span> <p>{props.item}</p> </li> ) MenuItem.propTypes = { price: React.PropTypes.number };
شیء propTypes را به کامپوننت اضافه کردهایم و در تنظیمات آن میتوانیم برای هر پارامتر ورودی یکی از اعضای PropTypes از React را که مناسب حال پارامتر است، انتخاب کنیم. در مثال بالا مشخص کردهایم که ورودی price باید عدد باشد و اگر مثلا رشتهای را بجای عدد ارسال کنیم، پیام خطای زیر را در Console خواهیم داشت.
Warning: Failed prop type: Invalid prop `price` of type `string` supplied to `MenuItem`, expected `number`. in MenuItem (created by Menu) in Menu
یا میتوانستیم از React.PropTypes.number.isRequired استفاده کنیم تا درج مقداری برای این ورودی الزامی باشد. اگر اعتبارسنجیهای React کافی نبودند میتوانیم اعتبارسنجیهای سفارشی خودمان را ایجاد کنیم. در مثال زیر میخواهیم ورودی price بیشتر از 15000 نباشد.
MenuItem.propTypes = { price: (props, price)=>{ if(props[price] > 15000){ return new Error("Too expensive!"); } } };
let MenuItem = React.createClass({ propTypes: { price: React.PropTypes.number } }); class MenuItem extends React.Component{ static propTypes = { price: React.PropTypes.number }; }
نوعهای دیگر برای اعتبارسنجی شامل موارد زیر هستند و البته مرجع تمام اعتبارسنجیهای React را میتوانید در اینجا بررسی کنید.
یکی از امکانات مفید دیگر برای مدیریت مقدارهای ورودی، مشخص کردن مقدار پیشفرضی برای یک پارامتر است. برای مثال اگر برای قیمت یک نوشیدنی مقداری وارد نشد، یک حداقل قیمت برای آن در نظر بگیریم، هر چند که ایده و روشی اشتباه است!
MenuItem.defaultProps = { price: 1000 };
همانطور که میبینید روش کار مشابه با اعتبارسنجی است و برای مشخص کردن مقدار پیشفرض برای React.creatClass از متد getDefaultProps که عضوی از React است، استفاده میکنیم.
let MenuItem = React.createClass({ getDefaultProps() { return { price: 200 } }, render() { return ( <li className="list-group-item"> <span className="badge">{this.props.price}</span> <p>{this.props.item}</p> </li> ); } });
using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Linq; using System.Linq.Expressions; namespace Sample { public abstract class BaseEntity { public int Id { set; get; } } public class Receipt : BaseEntity { public int TotalPrice { set; get; } } public class MyContext : DbContext { public DbSet<Receipt> Receipts { get; set; } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { if (!context.Receipts.Any()) { for (int i = 0; i < 20; i++) { context.Receipts.Add(new Receipt { TotalPrice = i }); } } base.Seed(context); } } public static class EFUtils { public static IList<T> LoadEntities<T>(this DbContext ctx, Expression<Func<T, bool>> predicate) where T : class { return ctx.Set<T>().Where(predicate).ToList(); } public static IList<T> LoadData<T>(this DbContext ctx, Func<T, bool> predicate) where T : class { return ctx.Set<T>().Where(predicate).ToList(); } } public static class Test { public static void RunTests() { startDB(); using (var context = new MyContext()) { var list1 = context.LoadEntities<Receipt>(x => x.TotalPrice == 10); var list2 = context.LoadData<Receipt>(x => x.TotalPrice == 20); } } private static void startDB() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); // Forces initialization of database on model changes. using (var context = new MyContext()) { context.Database.Initialize(force: true); } } } }
SELECT [Extent1].[Id] AS [Id], [Extent1].[TotalPrice] AS [TotalPrice] FROM [dbo].[Receipts] AS [Extent1] WHERE 10 = [Extent1].[TotalPrice]
SELECT [Extent1].[Id] AS [Id], [Extent1].[TotalPrice] AS [TotalPrice] FROM [dbo].[Receipts] AS [Extent1]
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate); public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
using System; using System.Collections; class ProgrArrayListExample { static void Main() { ArrayList list = new ArrayList(); list.Add("Hello"); list.Add(5); list.Add(3.14159); list.Add(DateTime.Now); for (int i = 0; i < list.Count; i++) { object value = list[i]; Console.WriteLine("Index={0}; Value={1}", i, value); } } }
Index=0; Value=Hello Index=1; Value=5 Index=2; Value=3.14159 Index=3; Value=29.02.2015 23:17:01
ArrayList list = new ArrayList(); list.Add(2); list.Add(3.5f); list.Add(25u); list.Add(" ریال"); dynamic sum = 0; for (int i = 0; i < list.Count; i++) { dynamic value = list[i]; sum = sum + value; } Console.WriteLine("Sum = " + sum); // Output: Sum = 30.5ریال
GenericType<T> instance = new GenericType<T>();
List<int> intList = new List<int>(); List<bool> boolList = new List<bool>(); List<double> realNumbersList = new List<double>();
List<int> intList = new List<int>();
استفاده از لیست پیوندی برای پیاده سازی پشته:
Stack<string> stack = new Stack<string>(); stack.Push("A"); stack.Push("B"); stack.Push("C"); while (stack.Count > 0) { string letter= stack.Pop(); Console.WriteLine(letter); } //خروجی //C //B //A
ابتدای آرایه مکانی است که عنصر از آنجا برداشته میشود و Head به آن اشاره میکند و tail هم به انتهای آرایه که جهت درج عنصر جدید مفید است. با برداشتن هر خانهای که head به آن اشاره میکند، head یک خانه به سمت جلو حرکت میکند و زمانی که Head از tail بیشتر شود، یعنی اینکه دیگر عنصری یا المانی در صف وجود ندارد و head و Tail به ابتدای صف حرکت میکنند. در این حالت موقعی که المان جدیدی قصد اضافه شدن داشته باشد، افزودن، مجددا از اول صف آغاز میشود و به این صفها، صف حلقوی میگویند.
عملیات اصلی صف دو مورد هستند enqueue که المان جدید را در انتهای صف قرار میدهد و dequeue اولین المان صف را بیرون میکشد.
پیاده سازی صف به صورت پویا با لیستهای پیوندی
برای پیاده سازی صف، لیستهای پیوندی یک طرفه کافی هستند:
در این حالت عنصر جدید مثل سابق به انتهای لیست اضافه میشود و برای حذف هم که از اول لیست کمک میگیریم و با حذف عنصر اول، متغیر Head به عنصر یا المان دوم اشاره خواهد کرد.
کلاس از پیش آمده صف در دات نت <Queue<T است و نحوهی استفاده آن بدین شکل است:
static void Main() { Queue<string> queue = new Queue<string>(); queue.Enqueue("Message One"); queue.Enqueue("Message Two"); queue.Enqueue("Message Three"); queue.Enqueue("Message Four"); while (queue.Count > 0) { string msg = queue.Dequeue(); Console.WriteLine(msg); } } //خروجی //Message One //Message Two //Message Thre //Message Four
dotnet tool install -g dotnet-ignore
dotnet tool list -g
dotnet tool update -g dotnet-ignore
dotnet tool uninstall -g dotnet-ignore
<PropertyGroup> <PackAsTool>true</PackAsTool> <ToolCommandName>dotnet-mytool</ToolCommandName> <PackageOutputPath>./nupkg</PackageOutputPath> </PropertyGroup>
dotnet tool install --global --add-source ./nupkg globaltools
dotnet-mytool
https://www.nuget.org/packages/McMaster.Extensions.CommandLineUtils
[Command(Description="Add a new note")] public class NewNote { [Required] [Option(Description="title of note")] public string Title{ get; set; } [Option(Description="content of note")] public string Body{ get; set; } }
[Command(Description="Add a new note")] public class NewNote:BaseClass { [Required] [Option(Description="title of note")] public string Title{ get; set; } [Option(Description="content of note")] public string Body{ get; set; } public void OnExecute(IConsole console) { var dir = GetBaseDirectory(); if(!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } var filePath = Path.Combine(dir, Title + ".txt"); File.WriteAllText(filePath, Body); console.WriteLine("the note is saved"); } }
public class BaseClass { protected string GetBaseDirectory(){ var baseDirectory = Environment.CurrentDirectory; return (Path.Combine(baseDirectory, "notes")); } }
public class List:BaseClass { [Option(Description="search a phrase in notes title")] public string Grep{ get; set; } public void OnExecute(IConsole console) { try { var baseDirectory = GetBaseDirectory(); var dir = new DirectoryInfo(baseDirectory); var files = dir.GetFiles(); foreach(var file in files) { if(!String.IsNullOrEmpty(Grep) && !file.Name.Contains(Grep)) continue; console.WriteLine(Path.GetFileNameWithoutExtension(file.Name)); } } catch (Exception e) { console.WriteLine(e.Message); } } }
[Command(Description="show contnet of note")] public class Show:BaseClass { [Required] [Option(Description="title of note")] public string Title{ get; set; } public void OnExecute(IConsole console){ var baseDirectory = GetBaseDirectory(); var file = Path.Combine(baseDirectory, Title+".txt"); if(!File.Exists(file)) { console.WriteLine("The Note NotFound..."); return; } console.WriteLine(File.ReadAllText(file)); } }
[Command(Description="An Immediate Note Saver")] [Subcommand(typeof(NewNote),typeof(List),typeof(Show))] class Program { static int Main(string[] args) { return CommandLineApplication.Execute<Program>(args); } public int OnExecute(CommandLineApplication app, IConsole console) { console.WriteLine("You must specify a subcommand."); console.WriteLine(); app.ShowHelp(); return 1; } }
static int Main(string[] args) { return CommandLineApplication.Execute<Program>(args); }
PS D:\projects\Samples\globaltools> dotnet-notes new-note -t "sample1" -b "this is body" the note is saved PS D:\projects\Samples\globaltools> dotnet-notes new-note -t "test1" -b "this is body of another note" the note is saved PS D:\projects\Samples\globaltools> dotnet-notes list sample1 test1 PS D:\projects\Samples\globaltools> dotnet-notes list -g sa sample1 PS D:\projects\Samples\globaltools> dotnet-notes show -t sample1 this is body
این کلاسها که موارد داخل پرانتز آنها اختیاری است، المان را تبدیل به یک المان Flexbox میکنند. حالت نمایشی پیشفرض آنها block است؛ اما اگر نیاز بود میتوان آنها را تبدیل به in-line نیز کرد. بنابراین سادهترین Flex Container را میتوان با افزودن کلاس d-flex ایجاد کرد.
به این ترتیب میتوان آیتمها را به صورت ردیفها و یا ستونهایی، نمایش داد. مقدار row در اینجا به صورت پیشفرض اعمال میشود. بنابراین ذکر کلاس خالی flex به معنای قرار دادن المانها در طی چندین ردیف در صفحه است. در اینجا استفادهی از reverse، نمایش المانها را از راست به چپ میسر میکند.
این مورد را در مطلب «طرحبندی صفحات وب با بوت استرپ 4 - قسمت دوم » بررسی کردیم. کلاس order را علاوه بر ستونها، بر روی هر دربرگیرندهای که دارای کلاس d-flex است نیز میتوان اعمال کرد.
برای مثال استفادهی از مقدار تراز center، روش بسیار مناسبی برای قرار دادن عناصر، در میانهی افقی صفحه است. این مورد را نیز در قسمت قبل بررسی کردیم.
که در اینجا دو مقدار wrap و nowrap قابل تنظیم است. در حالت wrap، اگر آیتمها با اندازهی خودشان در ردیف جاری جا نشدند، به سطر یا سطرهای بعدی منتقل خواهند شد. حالت پیشفرض nowrap است.
این مورد را نیز در قسمت قبل بررسی کردیم و همانند کار با ستونها میباشد.
<head> <style> .item { background: #f0ad4e; text-align: center; width: 150px; height: 30px; border: 1px solid white; } </style> </head> <body> <div class="container bg-danger"> <div class="bg-info" style="height:100vh"> <div class="item">Exotic</div> <div class="item">Grooming</div> <div class="item">Health</div> <div class="item">Nutrition</div> <div class="item">Pests</div> <div class="item">Vaccinations</div> </div> </div> </body>
<div class="bg-info d-flex" style="height:100vh">
<div class="bg-info d-flex flex-column" style="height:100vh">
<div class="bg-info d-flex flex-row-reverse" style="height:100vh">
<div class="bg-info d-flex flex-sm-column-reverse" style="height:100vh">
<div class="bg-info d-flex flex-sm-row-reverse justify-content-center" style="height:100vh">
<div class="bg-info d-flex flex-sm-row-reverse justify-content-center flex-wrap" style="height:100vh">
<div class="bg-info d-flex flex-sm-row-reverse justify-content-center flex-nowrap" style="height:100vh">
<div class="bg-info d-flex flex-sm-row-reverse justify-content-center flex-wrap align-content-start" style="height:100vh">
فرض کنید به تمام آیتمهای داخل Flex Container کلاس float-left را اضافه کردهایم. در این حالت Container قابلیت ردیابی اندازهی این آیتمها را از دست میدهد. به همین جهت با اعمال کلاس clearfix بوت استرپ به container، مجددا امکان ردیابی این آیتمها را پیدا میکند.
ابتدا با تعریف یک خاصیت شروع میشود؛ مانند m یا p، برای کنترل margin و padding. سپس لبهای که باید به آن اعمال شود بدون فاصله و یا - ذکر میشود؛ مانند mt به معنای margin-top. در این فرمول x به معنای اعمال همزمان به چپ و راست است و y به معنای اعمال همزمان به بالا و پایین و اگر میخواهید آیتمهای کناری آیتم جاری را به دو طرف لبهها هدایت کنید از mx-auto استفاده کنید.
<head> <style> .item { background: #f0ad4e; text-align: center; border: 1px solid white; } </style> </head> <body> <div class="container bg-danger"> <div class="bg-info d-flex"> <div class="item">Exotic</div> <div class="item">Grooming</div> <div class="item bg-danger ml-3 my-sm-3 pb-3 pt-5">Health</div> <div class="item">Nutrition</div> <div class="item">Pests</div> <div class="item">Vaccinations</div> </div> </div> </body>
برای مثال با انتساب کلاس d-sm-none به المانی، میتوان سبب مخفی شدن آن پس از گذر از sm شد.
import { Component, Input, OnInit } from '@angular/core'; import { AbstractControl } from '@angular/forms'; @Component({ selector: 'validation-message', template: ` <ng-container *ngIf="control.invalid && control.touched"> {{ message }} </ng-container> ` }) export class ValidationMessageComponent implements OnInit { @Input() control: AbstractControl; @Input() fieldDisplayName: string; @Input() rules: { [key: string]: string }; get message(): string { return this.control.hasError('required') ? `${this.fieldDisplayName} را وارد نمائید.` : this.control.hasError('pattern') ? `${this.fieldDisplayName} را به شکل صحیح وارد نمائید.` : this.control.hasError('email') ? `${this.fieldDisplayName} را به شکل صحیح وارد نمائید.` : this.control.hasError('minlength') ? `${this.fieldDisplayName} باید بیشتر از ${ this.control.errors.minlength.requiredLength } کاراکتر باشد.` : this.control.hasError('maxlength') ? `${this.fieldDisplayName} باید کمتر از ${ this.control.errors.maxlength.requiredLength } کاراکتر باشد.` : this.control.hasError('min') ? `${this.fieldDisplayName} باید بیشتر از ${ this.control.errors.min.requiredLength } باشد.` : this.control.hasError('max') ? `${this.fieldDisplayName} باید کمتر از ${ this.control.errors.max.requiredLength } باشد.` : this.hasRule() ? this.findRule() : this.control.hasError('model') ? `${this.control.errors.model.messages[0]}` : ''; } constructor() {} private hasRule() { return ( this.rules && Object.keys(this.control.errors).some(ruleKey => this.rules[ruleKey] ? true : false ) ); } private findRule(): string { let message = ''; Object.keys(this.control.errors).forEach(ruleKey => { if (this.rules[ruleKey]) { message += `${this.rules[ruleKey]} `; } }); return message; } ngOnInit(): void {} }
<mat-error *ngIf="form.controls['userName'].invalid && form.controls['userName'].touched" class="mat-text-warn"> <validation-message [control]="form.controls['userName']" fieldDisplayName="نام کاربری" [rules]="{rule1:'پیغام متناظر با rule1'}"> </validation-message> </mat-error>
this.form = this.formBuilder.group({ userName: [ '', [Validators.required, UserNameValidators.rule1)] ], password: ['', Validators.required], rememberMe: [false] }); export class UserNameValidators{ static rule1(control: AbstractControl) { if (control.value.indexOf(' ') >= 0) { return { rule1: true }; } return null; } }
3. کد جاوا اسکریپت خود را تولید کنید (تمام سرویس دهندگان این امکان را پیاده سازی کرده اند).
4. قطعه کد جاوا اسکریپت تولید شده را، در قسمت <head> فایل (Layout.cshtml (Razor_ یا (Site.Master (ASPX کپی کنید.
5. CSS Selectorهای لازم برای فونت مورد نظر را تولید کنید.
6. کد css تولید شده را در فایل Site.css کپی کنید. در مثال جاری فونت کل اپلیکیشن را تغییر میدهیم. برای اینکار، خانواده فونت "bistro-script-web" را به تگ body اضافه میکنیم.
نکته: فونت cursive بعنوان fallback تعریف شده. یعنی در صورتی که بارگذاری و رندر فونت مورد نظر با خطا مواجه شد، از این فونت استفاده میشود. بهتر است فونتهای fallback به مقادیری تنظیم کنید که در اکثر پلتفرمها وجود دارند.
همین! حالا میتوانیم تغییرات اعمال شده را مشاهده کنیم. بصورت پیش فرض قالب پروژهها از فونت "Segoe UI" استفاده میکنند، که خروجی زیر را رندر میکند.
استفاده از فونت جدید "Bistro Script Web" وب سایت را مانند تصویر زیر رندر خواهد کرد.
همانطور که میبینید استفاده از فونتهای وب بسیار ساده است. اما بهتر است از اینگونه فونتها برای کل سایت استفاده نشود و تنها روی المنتهای خاصی مانند سر تیترها (h1,h2,etc) اعمال شوند.
public abstract class GlobalData<T> where T : GlobalData<T>, new()
public static T Config { get; set; }
private static string _filename { get; set; }
public static void Init(string FileName = "AppConfig.json") { _filename = FileName; if (File.Exists(FileName)) { string json = File.ReadAllText(FileName); Config = (string.IsNullOrEmpty(json) ? new T() : JsonSerializer.Deserialize<T>(json)) ?? new T(); } else { Config = new T(); } }
public static void Save() { JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = true, IgnoreNullValues = true }; string json = JsonSerializer.Serialize(Config, options); File.WriteAllText(_filename, json); }
internal class AppConfig : GlobalData<AppConfig>
public string ServerUrl { get; set; } = "https://sub.deltaleech.com"; public bool IsShowNotification { get; set; } = true; public NavigationViewPaneDisplayMode PaneDisplayMode { get; set; } = NavigationViewPaneDisplayMode.Left; public SkinType Skin { get; set; } = SkinType.Default;
protected override void OnStartup(StartupEventArgs e) { GlobalData<AppConfig>.Init(); }
var skin = GlobalData<AppConfig>.Config.Skin;
GlobalData<AppConfig>.Config.Skin = Skin.Dark; GlobalData<AppConfig>.Save();
دقت داشته باشید که اگر بعد از ذخیره کردن تنظیمات، قصد داشته باشید اطلاعات جدید را دریافت کنید، باید حتما قبل از دریافت اطلاعات، متد Init را یکبار دیگر فراخوانی کنید تا اطلاعات جدید، نمایش داده شود.