در قسمت قبلی به اصل وجودی CLR پرداختیم. در این قسمت تا حدودی به بررسی ماژول مدیریت شده managed module که از زبانهای دیگر، کامپایل شده و به زبان میانی تبدیل گشته است صحبت میکنیم.
یک ماژول مدیریت شده شامل بخشهای زیر است:
نام بخش | توضیح |
هدر PE32 یا PE32+ | CLR باید بداند که برنامهی نوشته شده قرار است روی چه پلتفرمی و
با چه معماری، اجرا گردد. این برنامه یک برنامهی 32 بیتی است یا 64
بیتی. همچنین این هدر اشاره میکند که نوع فایل از چه نوعی است؛ GUI,CUI یا
DLL. به علاوه تاریخ ایجاد یا کامپایل فایل هم در آن ذکر شده است. در صورتیکه
این فایل شامل کدهای بومی native CPU هم باشد، اطلاعاتی در مورد این نوع
کدها نیز در این هدر ذکر میشود و اگر ماژول ارائه شده تنها شامل کد IL باشد،
قسمت بزرگی از اطلاعات این هدر در نظر گرفته نمیشود. |
CLR Header | اطلاعاتی را در مورد CLR ارائه میکند. اینکه برای
اجرا به چه ورژنی از CLR نیاز دارد. منابع مورد استفاده. آدرس و اندازه جداول
و فایلهای متادیتا و جزئیات دیگر. |
metadata | هر کد یا ماژول مدیریت شدهای، شامل جداول متادیتا است که این جداول
بر دو نوع هستند. اول جداولی که نوعها و اعضای تعریف شده در کد را توصیف
میکنند و دومی جداولی که نوعها و اعضایی را که در کد به آن ارجاع شده است،
توصیف میکنند. |
IL Code | اینجا محل قرار گیری کدهای میانی تبدیل شده است که در زمان اجرا، CLR آنها را به کدهای بومی تبدیل میکند. |
کامپایلرهایی که بر اساس CLR کار میکنند، وظیفه دارند جداول متادیتاها را به طور کامل ساخته و داخل فایل نهایی embed کنند. متادیتاها مجموعهی کاملی از فناوریهای قدیمی چون فایلهای COM یا Component Object Model و همچنین IDL یا Interface Definition (Description) Language هستند. گفتیم که متادیتاها همیشه داخل فایل IL که ممکن است DLL باشد یا EXE، ترکیب یا Embed شدهاند و جدایی آنها غیر ممکن است. در واقع کامپایلر در یک زمان، هم کد IL و هم متادیتاها را تولید کرده و آنها را به صورت یک نتیجهی واحد در میآورد.
متادیتاها استفادههای زیادی دارند که در زیر به تعدادی از آنان اشاره میکنیم:
- موقع کامپایل نیاز به هدرهای C و ++C از بین میرود؛ چرا که فایل نهایی شامل تمامی اطلاعات ارجاع شده میباشد. کامپایلرها میتوانند مستقیما اطلاعات را از داخل متادیتاها بخوانند.
- ویژوال استودیو از آنها برای کدنویسی راحتتر بهره میگیرد. با استفاده از قابلیت IntelliSense، متادیتاها به شما خواهند گفت چه متدهایی، چه پراپرتیهایی، چه رویدادهایی و ... در دسترس شماست و هر متد انتظار چه پارامترهایی را از شما دارد.
- CLR Code Verification از متادیتا برای اینکه اطمینان کسب کند که کدها تنها عملیات type Safe را انجام میدهند، استفاده میکند.
- متادیتاها به فیلد یک شیء اجازه میدهند که خود را به داخل بلوکهای حافظ انتقال داده و بعد از ارسال به یک ماشین دیگر، همان شیء را با همان وضعیت، ایجاد نماید.
- متادیتاها به GC اجازه میدهند که طول عمر یک شیء را رصد کند. GC برای هر شیء موجود میتواند نوع هر شیء را تشخیص داده و از طریق متادیتاها میتواند تشخیص دهد که فیلدهای یک شیء به اشیاء دیگری هم متصل هستند.
در آینده بیشتر در مورد متادیتاها صحبت خواهیم کرد.
ObservableCollection در Entity Framework
- با توجه به اینکه برای بررسی کارهای شخصی و کتابخانههای مستقل، نیاز به کد کامل هست، بهتر است به مقاله زیر مراجعه کرده و جزئیات کار خودتان را با آن مقایسه کنید:
«Implementing Undo/Redo feature for DbContext of Entity Framework»
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت ششم - تکمیل مستندات محافظت از API
<PackageReference Include="Swashbuckle.AspNetCore" Version="4.0.1" /> <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="4.0.1" /> <PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="4.5.2" />
public static class ServiceCollectionExtensions { public static void AddCustomSwagger(this IServiceCollection services) { services.AddSwaggerGen(options => { options.EnableAnnotations(); options.DocumentFilter<AuthenticationDocumentFilter>(); options.SwaggerDoc("v1", new Info { Version = "v1", Title = "Test API" }); }); } public static void UseSwaggerAndUI(this IApplicationBuilder app) { app.UseSwagger(); app.UseSwaggerUI(options => { options.DocExpansion(DocExpansion.None); options.SwaggerEndpoint("/swagger/v1/swagger.json", "Test API Docs"); }); } }
public class AuthenticationDocumentFilter : IDocumentFilter { private readonly IHttpContextAccessor httpContextAccessor; public AuthenticationDocumentFilter(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor; } public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context) { if (!httpContextAccessor.HttpContext.User.Identity.IsAuthenticated) { swaggerDoc.Definitions = new Dictionary<string, Schema>(); swaggerDoc.Paths = new Dictionary<string, PathItem>(); } } }
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context) { if (!httpContextAccessor.HttpContext.User.Identity.IsAuthenticated) { foreach (var item in swaggerDoc.Paths) { item.Value.Post = null; item.Value.Put = null; } } }
public void ConfigureServices(IServiceCollection services) { services.AddHttpContextAccessor(); services.AddAuthorization(); services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options=> { options.AccessDeniedPath = "/Login"; options.Cookie.HttpOnly = true; options.LoginPath = "/Login"; options.LogoutPath = "/Login"; options.ExpireTimeSpan = TimeSpan.FromDays(15); options.SlidingExpiration = true; options.Cookie.IsEssential = true; options.ReturnUrlParameter = "returnUrl"; }); services.AddMvc(); services.AddCustomSwagger(); }
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseAuthentication(); app.UseSwaggerAndUI(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
برای شروع همانند سایر مطالب میتوانید از این پست جهت راه اندازی هاست سرویسهای Web Api استفاده نمایید. برای فعال سازی قابلیت batching Request نیاز به یک MessageHandler داریم تا بتوانند درخواستهایی از این نوع را پردازش نمایند. خوشبختانه به صورت پیش فرض این Handler پیاده سازی شدهاست و ما فقط باید آن را با استفاده از متد MapHttpBatchRoute به بخش مسیر یابی (Route Handler) پروژه معرفی نماییم.
public class Startup { public void Configuration(IAppBuilder appBuilder) { var config = new HttpConfiguration(); config.Routes.MapHttpBatchRoute( routeName: "Batch", routeTemplate: "api/$batch", batchHandler: new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer)); config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "Default", routeTemplate: "{controller}/{action}/{name}", defaults: new { name = RouteParameter.Optional } ); config.Formatters.Clear(); config.Formatters.Add(new JsonMediaTypeFormatter()); config.Formatters.JsonFormatter.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented; config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); config.EnsureInitialized(); appBuilder.UseWebApi(config); } }
مهمترین نکتهی آن استفاده از DefaultHttpBatchHandler و معرفی آن به بخش batchHandler مسیریابی است. کلاس DefaultHttpBatchHandler برای وهله سازی نیاز به آبجکت سروری که سرویسهای WebApi در آن هاست شدهاند دارد که با دستور GlobalConfiguration.DefaultServer به آن دسترسی خواهید داشت. در صورتی که HttpServer خاص خود را دارید به صورت زیر عمل نمایید:
var config = new HttpConfiguration(); HttpServer server = new HttpServer(config);
using System.Net.Http; using System.Net.Http.Formatting; public class Program { private static void Main(string[] args) { string baseAddress = "http://localhost:8080"; var client = new HttpClient(); var batchRequest = new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/$batch") { Content = new MultipartContent("mixed") { new HttpMessageContent(new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/Book/Add") { Content = new ObjectContent<string>("myBook", new JsonMediaTypeFormatter()) }), new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/Book/GetAll")) } }; var batchResponse = client.SendAsync(batchRequest).Result; MultipartStreamProvider streamProvider = batchResponse.Content.ReadAsMultipartAsync().Result; foreach (var content in streamProvider.Contents) { var response = content.ReadAsHttpResponseMessageAsync().Result; } } }
برای دریافت پاسخ این گونه درخواستها نیز از متد الحاقی ReadAsMultipartAsync استفاده میشود که امکان پیمایش بر بدنهی پیام دریافتی را میدهد.
مدیریت ترتیب درخواست ها
شاید این سوال به ذهن شما نیز خطور کرده باشد که ترتیب پردازش این گونه پیامها چگونه خواهد بود؟ به صورت پیش فرض ترتیب اجرای درخواستها حائز اهمیت است. بعنی تا زمانیکه پردازش درخواست اول به اتمام نرسد، کنترل اجرای برنامه، به درخواست بعدی نخواهد رسید که این مورد بیشتر زمانی رخ میدهد که قصد دریافت اطلاعاتی را داشته باشید که قبل از آن باید عمل Persist در پایگاه داده اتفاق بیافتد. اما در حالاتی غیر از این میتوانید این گزینه را غیر فعال کرده تا تمام درخواستها به صورت موازی پردازش شوند که به طور قطع کارایی آن نسبت به حالت قبلی بهینهتر است.
برای غیر فعال کردن گزینهی ترتیب اجرای درخواستها، به صورت زیر عمل نمایید:
config.Routes.MapHttpBatchRoute( routeName: "WebApiBatch", routeTemplate: "api/$batch", batchHandler: new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer) { ExecutionOrder = BatchExecutionOrder.NonSequential });
برای طراحی گزارش شما میتوانید به سه روش این کار را انجام دهید.
1- طراحی در برنامه طراح گزارش
2- طراحی از داخل ویژوال استودیو
3- طراحی گزارش در زمان اجرا
برای شروع شما میتوانید نسخه آزمایشی این گزارشساز را دریافت کنید. تنها محدودیت این نسخه نمایش عبارت Demo در چاپ میباشد.
برنامه Designer را اجرا کنید. در صورتی که برای اولین بار است این برنامه را اجرا میکنید ابتدا باید رابط کاربری خود را انتخاب نمایید. نوار ابزار سمت چپ تمامی ابزارهای پرکاربرد طراحی گزارش را در اختیارتان قرار میدهد. ابزارهایی که در این بخش درباره آنها توضیح داده خواهد شد عبارتند از:
Header, Footer, Data, Page Header, Page Footer, Report Title, Report Summery
*به ابزارهای بالا Band گفته میشود.
Header , Footer :
همانطور که از نامشان پیداست در قسمت بالا و پایین بخشی از گزارش قرار میگیرند که برای استفاده در بالا و پایین بند Data میباشد. به عنوان مثال بند Header مناسب طراحی سرستونهای یک جدول میباشد و بند Footer هم جهت نمایش اطلاعات انتهایی یک جدول. ولی شما میتوانید با تنظیم خصوصیات هر بند رفتار و نمایش آنها را به طور کل تغییر دهید. نکته مثبت این گزارشساز این است که شما میتوانید بیش از یک واحد از هر بند را بر روی صفحه طراح خود قرار دهید، به عنوان مثال شما میتوانید دو بند Header داشته باشید که یکی در صفحات زوج و دیگری در صفحات فرد نمایش داده شود.
Data :
این بند جهت نمایش اطلاعات از منبع دادهها میباشد. به این معنا که به ازای هر سطر از دادهها یک بار این بخش نمایش داده میشود. تعداد دفعات نمایش این بند محدود به تعداد سطرهای منبع داده و یا اندازه صفحه و همچنین خصوصیت محدوده نمایش سطرها در یک صفحه میباشد.
Page Header , Page Footer :
این دو بند با توجه به نامشان جهت نمایش در بالا و پایین هر صفحه از گزارش میباشد. البته باز هم یادآور میشوم که با تغییر در خصوصیاتشان میتوانید رفتار و نحوه نمایش آنها را تغییر دهید.
Report Title :
این بند فقط در ابتدای گزارش نمایش داده خواهد شد.
Report Summery :
این بند بلافاصله بعد از اتمام گزارش نمایش داده خواهد شد.
مثال :
برای شروع در Designer یک گزارش جدید از نوع Blank Report ایجاد نمایید. سپس در پنل Dictionary بر روی New Item کلیک کرده و گزینه XML Data را انتخاب نمایید. با توجه به محل نصب گزارشساز وارد مسیر …\Bin\Data شده و فایلهای Demo.xsd و Demo.xml را برای قسمتهای مربوطه انتخاب نمایید. یک بار دیگر بر رو New Item کلیک کرده و گزینه New Data Source را انتخاب نمایید، از لیست ظاهر شده کانکشنی را که ایجاد کردهاید را انتخاب نمایید؛ نتیجه کار تا اینجا باید به صورت زبر باشد.
جدول Product را دراگ کرده و بر روی صفحه طراحی گزارش رها کنید. فرم Data ظاهر میشود این فرم را مطابق تصویر زیر تنظیم نمایید.
حال بر روی صفحه طراحی گزارش بندهای Header, Data, Footer مشاهده میشود؛ حال شما میتوانید با کلیک بر روی سربرگ Preview خروجی گزارش را ببینید.
توابع :
این گزارشساز دارای توابع بسیاری است که اکثر نیازهای شما را برطرف میکند به عنوان مثال تابع تبدیل عدد به حروف به زبان فارسی. همچنین شما میتوانید توابع خاص خود را ساخته و به صورت رفرنس به گزارش اضافه نمایید.
در این بخش ما از توابع موجود در گزارش استفاده خواهیم کرد. برای شروع بر روی کامپوننت Text در بند Footer زیر ستون UnitPrice دابل کلیک کرده تا فرم TextEditor ظاهر شود. سربرگ Summery را انتخاب نمایید. مطابق اطلاعات زیر بخشها را تنظیم نمایید.
Summery Function: Sum
Data Band: DataProducts
Data Column: Products.UnitPrice
حال بر روی سربرگ Preview
کلیک نمایید تا خروجی گزارش را ببینید. جمع ستون UnitPrice
فقط در صفحه آخر نمایش داده خواهد شد. اگر بخواهید جمع ستون در پایین هر صفحه
نمایش داده شود ابتدا باید خصوصیت Print on All Pages بند Footer به True
ست شود. سپس بر روی کامپوننت Text در بند Footer،
دابل کلیک نمایید و در فرم TextEditor
سربرگ Summery
تیک Running Total
را به حالت انتخاب شده در بیاورید، حال خروجی گزارش را ببینید، جمع در انتهای هر
صفحه ظاهر میشود.
متغیرها :
در این گزارش ساز دو نوع متغیر وجود دارد؛ نرمال و سیستمی. نوع سیستمی شامل متغیرهایی میشود که کاربرد مشخصی در تهیه گزارش دارند، مثل شماره صفحه، شماره ردیف، عنوان گزارش و ...
برای مثال شما میتوانید متغیر سیستمی Line را برای روی صفحه طراحی دراگ کنید. دو کامپوننت Text بر روی صفحه ایجاد میشود. اولی با محتوای Line و دومی با محتوای {Line}. اولی را به بند Header و دومی را به بند Data منتقل کنید و سپس خروجی گزارش را مشاهده نمایید، حال گزارش شما دارای شماره ردیف است.
متغیرهای نرمال تقریبا همانند متغیرهایی هستند که همه روزه شما در برنامههای خود از آنها استفاده میکنید. با کلیک بر روی New Item گزینه New Variable را انتخاب نمایید و نوع متغیر را Decimal انتخاب نمایید، سپس متغیر ایجاد شده را دراگ کرده و بروی صفحه طراحی قرار دهید و مشابه متغیر Line عمل کرده و کامپوننتهای Text را در بندهای مناسب قرار دهید. سپس بند Data بر روی صفحه طراحی را انتخاب نمایید، در پنل Properties بر روی Eventes کلیک کرده سپس در رویداد Rendering کد زیر را وارد نمایید.
Variable1 += Products.UnitPrice
حال در خروجی گزارش میتوانید مقادیر محاسبه
شده را ببینید. توجه داشته باشید که شما میتوانید در رویدادهای این گزارشساز به
زبان VB
و C#
برنامه نویسی کنید و محدود به یک خط کد نمیباشید.
شما میتوانید گزارش ساخته شده را به صورتهای مختلف ذخیره کنید از جمله کد C# و یا یک اپلیکیشن قابل اجرا.
افزودن وابستگیهای MailKit به برنامه
برای شروع به استفادهی از MailKit، میتوان بستهی نیوگت آنرا به فایل project.json برنامه معرفی کرد:
{ "dependencies": { "MailKit": "1.10.0" } }
استفاده از MailKit جهت تکمیل وابستگیهای ASP.NET Core Identity
قسمتی از ASP.NET Core Identity، شامل ارسال ایمیلهای «ایمیل خود را تائید کنید» است که آنرا میتوان توسط MailKit به نحو ذیل تکمیل کرد:
using System.Threading.Tasks; using ASPNETCoreIdentitySample.Services.Contracts.Identity; using MailKit.Net.Smtp; using MailKit.Security; using MimeKit; namespace ASPNETCoreIdentitySample.Services.Identity { public class AuthMessageSender : IEmailSender, ISmsSender { public async Task SendEmailAsync(string email, string subject, string message) { var emailMessage = new MimeMessage(); emailMessage.From.Add(new MailboxAddress("DNT", "do-not-reply@dotnettips.info")); emailMessage.To.Add(new MailboxAddress("", email)); emailMessage.Subject = subject; emailMessage.Body = new TextPart(TextFormat.Html) { Text = message }; using (var client = new SmtpClient()) { client.LocalDomain = "dotnettips.info"; await client.ConnectAsync("smtp.relay.uri", 25, SecureSocketOptions.None).ConfigureAwait(false); await client.SendAsync(emailMessage).ConfigureAwait(false); await client.DisconnectAsync(true).ConfigureAwait(false); } } public Task SendSmsAsync(string number, string message) { // Plug in your SMS service here to send a text message. return Task.FromResult(0); } } }
در آخر، این پیام به SmtpClient جهت ارسال نهایی، فرستاده میشود. این SmtpClient هرچند هم نام مشابه آن در System.Net.Mail است اما با آن یکی نیست و متعلق است به MailKit. در اینجا ابتدا LocalDomain تنظیم شدهاست. تنظیم این مورد اختیاری بوده و صرفا به SMTP سرور دریافت کنندهی ایمیلها، مرتبط است که آیا قید آنرا اجباری کردهاست یا خیر. تنظیمات اصلی SMTP Server در متد ConnectAsync ذکر میشوند که شامل مقادیر host ،port و پروتکل ارسالی هستند.
ارسال ایمیل به SMTP pickup folder
روشی که تا به اینجا بررسی شد، جهت ارسال ایمیلها به یک SMTP Server واقعی کاربرد دارد. اما در حین توسعهی محلی برنامه میتوان ایمیلها را در داخل یک پوشهی موقتی ذخیره و آنها را توسط برنامهی Outlook (و یا حتی مرورگر Firefox) بررسی و بازبینی کامل کرد.
در این حالت تنها کاری را که باید انجام داد، جایگزین کردن قسمت ارسال ایمیل واقعی توسط SmtpClient در کدهای فوق، با قطعه کد ذیل است:
using (var stream = new FileStream($@"c:\smtppickup\email-{Guid.NewGuid().ToString("N")}.eml", FileMode.CreateNew)) { emailMessage.WriteTo(stream); }
FAQ و منبع تکمیلی
- بر روی پایگاه داده مورد نظر راست کلیک کرده و از گزینه Tasks گزینه Copy Database را انتخاب کنید.
- پس از ظاهر شدن پنجره کپی گزینه Next را انتخاب و در مرحله مبدا و مقصد، سرور جاری را انتخاب کنید و به مرحله بعد بروید.
این برای زمانی است که شما میخواهید پایگاه داده را در سرور دیگری کپی نماید - در پنجره Transfer Method دو روش Detach and Attach و استفاده از SQL Management Object وجود دارد که با همان روش اول به مرحله بعد بروید
- در مرحله بعد نام پایگاه داده شما انتخاب شده به مرحله بعد بروید.
- مرحله بعد پیکریندی پایگاه داده مقصد میباشد که نام و مسیر پایگاه داده جدید را میتوانید مشخص نمایید.
- این عملیات با SQL Server Agent صورت میپذیرد به همین خاطر Agent میبایست نصب و Start شده باشد.
- با انتخاب گزینه Next مراحل بعد را رد کرده تا عملیات آغاز شود.
- در مرحله آخر پایگاه داده قبلی را حذف نمایید.
زمانی که سیستم عامل های GUI مثل ویندوز به بازار آمدند، یکی از قسمتهای گرافیکی آنها AddressBar نام داشت که مسیر حرکت آنها را در فایل سیستم نشان میداد و در سیستم عاملهای متنی CLI با دستور cd یا pwd انجام میشد. بعدها در وب هم همین حرکت با نام BreadCrumb صورت گرفت که به عنوان مثال مسیر رسیدن به صفحهی یک محصول یا یک مقاله را نشان میداد. در یک پروژهی اندرویدی نیاز بود تا یک ساختار درختی را پیاده سازی کنم، ولی در برنامههای اندروید ایجاد یک درخت، کار هوشمندانه و مطلوبی نیست و روش کار به این صورت است که یک لیست از گروههای والد را نمایش داده و با انتخاب هر آیتم لیست به آیتمهای فرزند تغییر میکند. حالا مسئله این بود که کاربر باید مسیر حرکت خودش را بشناسد. به همین علت مجبور شدم یک BreadCrumb را برای آن طراحی کنم که در زیر تصویر آن را مشاهده میکنید.
از نکات جالب توجه در مورد این ماژول میتوان گفت که قابلیت این را دارد تا تصمیمات خود را بر اساس اندازههای مختلف صفحه نمایش بگیرد. به عنوان مثال اگر آیتمهای بالا بیشتر از سه عدد باشد و در صفحه جا نشود از یک مسیر جعلی استفاده میکند و همهی آیتمها با اندیس شماره 1 تا index-3 را درون یک آیتم با عنوان (...) قرار میدهد که من به آن میگویم مسیر جعلی. به عنوان نمونه مسیر تصویر بالا در صفحه جا شده است و نیازی به این کار دیده نشده است. ولی تصویر زیر از آن جا که مسیر، طول width صفحه نمایش رد کرده است، نیاز است تا چنین کاری انجام شود. موقعیکه کاربر آیتم ... را کلیک کند، مسیر باز شده و به محل index-3 حرکت میکند. یعنی دو مرحله به عقب باز میگردد.
نگاهی به کارکرد ماژول
قبل از توضیح در مورد سورس، اجازه دهید نحوهی استفاده از آن را ببینیم.
این سورس شامل دو کلاس است که سادهترین کلاس آن AndBreadCrumbItem میباشد که مشابه کلاس ListItem در بخش وب دات نت است و دو مقدار، یکی متن و دیگری Id را میگیرد:
سورس:
public class AndBreadCrumbItem { private int Id; private String diplayText; public AndBreadCrumbItem(int Id, String displayText) { this.Id=Id; this.diplayText=displayText; } public String getDiplayText() { return diplayText; } public void setDiplayText(String diplayText) { this.diplayText = diplayText; } public int getId() { return Id; } public void setId(int id) { Id = id; } }
به عنوان مثال میخواهیم یک breadcrumb را با مشخصات زیر بسازیم:
AndBreadCrumbItem itemhome=new AndBreadCrumbItem(0,"Home"); AndBreadCrumbItem itemproducts=new AndBreadCrumbItem(12,"Products"); AndBreadCrumbItem itemdigital=new AndBreadCrumbItem(15,"Digital"); AndBreadCrumbItem itemhdd=new AndBreadCrumbItem(56,"Hard Disk Drive");
AndBreadCrumb breadCrumb=new AndBreadCrumb(this); breadCrumb.AddNewItem(itemhome); breadCrumb.AddNewItem(itemproducts); breadCrumb.AddNewItem(itemdigital); breadCrumb.AddNewItem(itemhdd);
breadCumb.setContext(this);
پس از افزودن آیتم ها، تنظیمات زیر را اعمال نمایید:
LinearLayout layout=(LinearLayout)getActivity().findViewById(R.id.breadcumblayout); layout.setPadding(8, 8, 8, 8); breadCrumb.setLayout(layout); breadCrumb.SetTinyNextNodeImage(R.drawable.arrow); breadCrumb.setTextSize(25); breadCrumb.SetViewStyleId(R.drawable.list_item_style);
حال برای رسم آن متد UpdatePath را صدا میزنیم:
breadCrumb.UpdatePath();
الان اگر برنامه اجرا شود باید breadcrumb از چپ به راست رسم گردد. برای استفادههای فارسی، راست به چپ میتوانید از متد زیر استفاده کنید:
breadCrumb.setRTL(true);
در صورتیکه قصد دارید تنظیمات بیشتری چون رنگ متن، فونت متن و ... را روی هر المان اعمال کنید، از رویداد زیر استفاده کنید:
breadCrumb.setOnTextViewUpdate(new ITextViewUpdate() { @Override public TextView UpdateTextView(Context context, TextView tv) { tv.setTextColor(...); tv.setTypeface(...); return tv; } });
همچنین در صورتیکه میخواهید بدانید کاربر بر روی چه عنصری کلیک کرده است، از رویداد زیر استفاده کنید:
breadCumb.setOnClickListener(new IClickListener() { @Override public void onClick(int position, int Id) { //... } });
آخرین متد موجود که کمترین استفاده را دارد، متد SetNoResize است. در صورتیکه این متد با True مقداردهی گردد، عملیات تنظیم بر اساس صفحهی نمایش لغو میشود. این متد برای زمانی مناسب است که به عنوان مثال شما از یک HorozinalScrollView استفاده کرده باشید. در این حالت layout شما هیچ گاه به پایان نمیرسد و بهتر هست عملیات اضافه را لغو کنید.
نگاهی به سورس
کلاس زیر شامل بخشهای زیر است:
فیلدهای خصوصی
//=-=--=-=-=-=-=-=-=-=-=-=-=- Private Properties -=-=-=-=-=-=-=--=-=-= private List<AndBreadCrumbItem> items=null; private List<TextView> textViews; private int tinyNextNodeImage; private int viewStyleId; private Context context; private boolean RTL; private float textSize=20; private boolean noResize=false; LinearLayout layout; IClickListener clickListener; ITextViewUpdate textViewUpdate; LinearLayout.LayoutParams params ;
اینترفیسها هم با حرف I شروع و برای تعریف رویدادها ایجاد شدهاند. در ادامه از تعدادی متد get و Set برای مقدار دهی بعضی از فیلدهای خصوصی بالا استفاده شده است:
//=-=---=-=-=-=-- Constructor =--=-=-=-=-=--=-=- public AndBreadCrumb(Context context) { this.context=context; params = new LinearLayout.LayoutParams (LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); } //=-=-=--=--=-=-=-=-=-=-=-=- Public Properties --=-=-=-=-=-=--=-=-=-=-=-=- //each category would be added to create path public void AddNewItem(AndBreadCrumbItem item) { if(items==null) items=new ArrayList<>(); items.add(item); } // if you want a pointer or next node between categories or textviews public void SetTinyNextNodeImage(int resId) {this.tinyNextNodeImage=resId;} public void SetViewStyleId(int resId) {this.viewStyleId=resId;} public void setTextSize(float textSize) {this.textSize = textSize;} public boolean isRTL() { return RTL; } public void setRTL(boolean RTL) { this.RTL = RTL; } public void setLayout(LinearLayout layout) { this.layout = layout; } public void setContext(Context context) { this.context = context; } public boolean isNoResize() { return noResize; } public void setNoResize(boolean noResize) { this.noResize = noResize; }
بعد از آن به متدهای خصوصی میرسیم که متد زیر، متد اصلی ما برای ساخت breadcrumb است:
//primary method for render objects on layout private void DrawPath() { //stop here if essentail elements aren't present if (items == null) return ; if (layout == null) return; if (items.size() == 0) return; //we need to get size of layout,so we use the post method to run this thread when ui is ready layout.post(new Runnable() { @Override public void run() { //textviews created here one by one int position = 0; textViews = new ArrayList<>(); for (AndBreadCrumbItem item : items) { TextView tv = MakeTextView(position, item.getId()); tv.setText(item.getDiplayText()); textViews.add(tv); position++; } //add textviews on layout AddTextViewsOnLayout(); //we dont manage resizing anymore if(isNoResize()) return; //run this code after textviews Added to get widths of them TextView last_tv=textViews.get(textViews.size()-1); last_tv.post(new Runnable() { @Override public void run() { //define width of each textview depend on screen width BatchSizeOperation(); } }); } }); }
TextView tv=new TextView(this); tv.getWidth(); //return 0 layout.add(tv); tv.getWidth(); //return 0
TextView tv=new TextView(this); tv.post(new Runnable() { @Override public void run() { tv.getWidth(); //return x } });
باز میگردیم به متد DrawPath و داخل متد post
در اولین خط این پروسه به ازای هر آیتم، یک TextView توسط متد MakeTextView ساخته میشود که شامل کد زیر است:
private TextView MakeTextView(final int position, final int Id) { //settings for cumbs TextView tv=new TextView(this.context); tv.setEllipsize(TextUtils.TruncateAt.END); tv.setSingleLine(true); tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); tv.setBackgroundResource(viewStyleId); /*call custom event - this event will be fired when user click on one of textviews and returns position of textview and value that user sat as id */ tv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { SetPosition(position); clickListener.onClick(position, Id); } }); //if user wants to update each textviews if(textViewUpdate!=null) tv=textViewUpdate.UpdateTextView(context,tv); if(isRTL()) tv.setRotationY(180); return tv; }
در خطوط اولیه، یک Textview ساخته و متد Ellipsize را با Truncate.END مقداردهی مینماید. این مقدار دهی باعث میشود اگر متن، در Textview جا نشد، ادامهی آن با ... مشخص شود. در خط بعدی Textview را تک خطه معرفی میکنیم. در خط بعدی اندازهی قلم را بر اساس آنچه کاربر مشخص کرده است، تغییر میدهیم و بعد هم استایل را برای آن مقداردهی میکنیم. بعد از آن رویداد کلیک را برای آن مشخص میکنیم تا اگر کاربر بر روی آن کلیک کرد، رویداد اختصاصی خودمان را فراخوانی کنیم.
در خط بعدی اگر rtl با true مقدار دهی شده باشد، textview را حول محور Y چرخش میدهد تا برای زبانهای راست به چپ چون فارسی آماده گردد و در نهایت Textview ساخته شده و به سمت متد DrawPath باز میگرداند.
بعد از ساخته شدن TextViewها، وقت آن است که به Layout اضافه شوند که وظیفهی اینکار بر عهدهی متد AddTextViewOnLayout است:
//this method calling by everywhere to needs add textviews on the layout like master method :drawpath private void AddTextViewsOnLayout() { //prepare layout //remove everything on layout for recreate it layout.removeAllViews(); layout.setOrientation(LinearLayout.HORIZONTAL); layout.setVerticalGravity(Gravity.CENTER_VERTICAL); if(isRTL()) layout.setRotationY(180); //add textviews one by one int position=0; for (TextView tv:textViews) { layout.addView(tv,params); //add next node image between textviews if user defined a next node image if(tinyNextNodeImage>0) if(position<(textViews.size()-1)) { layout.addView(GetNodeImage(), params); position++; } } }
تا به اینجا کار چیدمان به ترتیب انجام شده است ولی از آنجا که اندازهی Layout در هر گوشی و در دو حالت حالت افقی یا عمودی نگه داشتن گوشی متفاوت است، نمیتوان به این چینش اعتماد کرد که به چه نحوی عناصر نمایش داده خواهند شد و این مشکل توسط متد BatchSizeOperation (تغییر اندازه دسته جمعی) حل میگردد. در اینجا هم باز متد post به آخرین textview اضافه شده است. به این علت که موقعیکه همهی textviewها در ui جا خوش کردند، بتوانیم به خاصیتهای ui آنها دستیابی داشته باشیم. حالا بعد از ترسیم باید اندازه آنها را اصلاح کنیم. قدم به قدم متد BatchSizeOperation را بررسی میکنیم:
//set textview width depend on screen width private void BatchSizeOperation() { //get width of next node between cumbs Bitmap tinyBmap = BitmapFactory.decodeResource(context.getResources(), tinyNextNodeImage); int tinysize=tinyBmap.getWidth(); //get sum of nodes tinysize*=(textViews.size()-1); ... }
//get width size of screen(layout is screen here) int screenWidth=GetLayoutWidthSize(); //get sum of arrows and cumbs width int sumtvs=tinysize; for (TextView tv : textViews) { int width=tv.getWidth(); sumtvs += width; }
private int GetLayoutWidthSize() { int width=layout.getWidth(); int padding=layout.getPaddingLeft()+layout.getPaddingRight(); width-=padding; return width; }
private void BatchSizeOperation() { .... //if sum of cumbs is less than screen size the state is good so return same old textviews if(sumtvs<screenWidth) return ; if(textViews.size()>3) { //make fake path MakeFakePath(); //clear layout and add textviews again AddTextViewsOnLayout(); } //get free space without next nodes -> and spilt rest of space to textviews count to get space for each textview int freespace =screenWidth-tinysize; int each_width=freespace/textViews.size(); //some elements have less than each_width,so we should leave size them and calculate more space again int view_count=0; for (TextView tv:textViews) { if (tv.getWidth()<=each_width) freespace=freespace-tv.getWidth(); else view_count++; } if (view_count==0) return; each_width=freespace/view_count; for (TextView tv:textViews) { if (tv.getWidth()>each_width) tv.setWidth(each_width); } }
اگر آیتمها بیشتر از سه عدد باشند، میتوانیم از حالت مسیر جعلی استفاده کنیم که توسط متد MakeFakePath انجام میشود. البته بعد از آن هم باید دوباره viewها را چینش کنیم تا مسیر جدید ترسیم گردد، چون ممکن است بعد از آن باز هم جا نباشد یا آیتمها بیشتر از سه عدد نیستند. در این حالت، حداقل کاری که میتوانیم انجام دهیم این است که فضای موجود را بین آنها تقسیم کنیم تا همهی کاسه، کوزهها سر آیتم آخر نشکند و متنش به ... تغییر یابد و حداقل از هر آیتم، مقداری از متن اصلی نمایش داده شود. پس میانگین فضای موجود را گرفته و بر تعداد المانها تقسیم میکنیم. البته این را هم باید در نظر گرفت که در تقسیم بندی، بعضی آیتمها آن مقدار پهنا را نیاز ندارند و با پهنای کمتر هم میشود کل متنشان را نشان داد. پس یک کار اضافهتر این است که مقدار پهنای اضافی آنها را هم حساب کنیم و فقط آیتمهایی را پهنا دهیم که به مقدار بیشتری از این میانگین احتیاج دارند. در اینجا کار به پایان میرسد و مسیر نمایش داده میشود.
نحوهی کارکرد متد MakeFakePath بدین صورت است که 4 عدد TextView را ایجاد کرده که المانهای با اندیس 0 و 2 و 3 به صورت نرمال و عادی ایجاد شده و همان کارکرد سابق را دارند. ولی المان شماره دو با اندیس 1 با متن ... نمایندهی آیتمهای میانی است و رویدادکلیک آن به شکل زیر تحریف یافته است:
//if elements are so much(mor than 3),we make a fake path to decrease elements private void MakeFakePath() { //we make 4 new elements that index 1 is fake element and has a rest of real path in its heart //when user click on it,path would be opened textViews=new ArrayList<>(4); TextView[] tvs=new TextView[4]; int[] positions= {0,items.size()-3,items.size()-2,items.size()-1}; for (int i=0;i<4;i++) { //request for new textviews tvs[i]=MakeTextView(positions[i],items.get(positions[i]).getId()); if(i!=1) tvs[i].setText(items.get(positions[i]).getDiplayText()); else { tvs[i].setText("..."); //override click event and change it to part of code to open real path by call setposition method and redraw path tvs[i].setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = items.size() - 3; int id = items.get(pos).getId(); SetPosition(items.size() - 3); clickListener.onClick(pos, id); } }); } textViews.add(tvs[i]); } }