مطالب
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 6#
در ادامه پست پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 5# ، در این پست به تشریح کلاس دایره و بیضی می‌پردازیم.

ابتدا به تشریح کلاس ترسیم بیضی (Ellipse) می‌پردازیم.
using System.Drawing;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Ellipse Draw
    /// </summary>
    public class Ellipse : Shape
    {
        #region Constructors (2)

        /// <summary>
        /// Initializes a new instance of the <see cref="Ellipse" /> class.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="zIndex">Index of the z.</param>
        /// <param name="foreColor">Color of the fore.</param>
        /// <param name="thickness">The thickness.</param>
        /// <param name="isFill">if set to <c>true</c> [is fill].</param>
        /// <param name="backgroundColor">Color of the background.</param>
        public Ellipse(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor)
            : base(startPoint, endPoint, zIndex, foreColor, thickness, isFill, backgroundColor)
        {
            ShapeType = ShapeType.Ellipse;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Ellipse" /> class.
        /// </summary>
        public Ellipse()
        {
            ShapeType = ShapeType.Ellipse;
        }

        #endregion Constructors

        #region Methods (1)

        // Public Methods (1) 

        /// <summary>
        /// Draws the specified g.
        /// </summary>
        /// <param name="g">The g.</param>
        public override void Draw(Graphics g)
        {
            if (IsFill)
                g.FillEllipse(BackgroundBrush, StartPoint.X, StartPoint.Y, Width, Height);
            g.DrawEllipse(Pen, StartPoint.X, StartPoint.Y, Width, Height);
            base.Draw(g);
        }

        #endregion Methods
    }
}
این کلاس از شی Shape ارث برده و دارای دو سازنده ساده می‌باشد که نوع شی ترسیمی را مشخص می‌کنند، در متد Draw نیز با توجه به توپر یا توخالی بودن شی ترسیم آن انجام میشود، در این کلاس باید متد HasPointInShape بازنویسی (override) شود، در این متد باید تعیین شود که یک نقطه در داخل بیضی قرار گرفته است یا خیر که متاسفانه فرمول بیضی خاطرم نبود. البته به صورت پیش فرض نقطه با توجه به چهارگوشی که بیضی را احاطه می‌کند سنجیده می‌شود.

کلاس دایره (Circle) از کلاس بالا (Ellipse) ارث بری دارد که کد آن را در زیر مشاهده می‌نمایید.
using System;
using System.Drawing;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Circle
    /// </summary>
    public class Circle : Ellipse
    {
#region Constructors (2) 

        /// <summary>
        /// Initializes a new instance of the <see cref="Circle" /> class.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="zIndex">Index of the z.</param>
        /// <param name="foreColor">Color of the fore.</param>
        /// <param name="thickness">The thickness.</param>
        /// <param name="isFill">if set to <c>true</c> [is fill].</param>
        /// <param name="backgroundColor">Color of the background.</param>
        public Circle(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor)
        {
            float x = 0, y = 0;
            float width = Math.Abs(endPoint.X - startPoint.X);
            float height = Math.Abs(endPoint.Y - startPoint.Y);
            if (startPoint.X <= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = startPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = endPoint.X;
                y = endPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = endPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X <= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = startPoint.X;
                y = endPoint.Y;
            }
            StartPoint = new PointF(x, y);
            var side = Math.Max(width, height);
            EndPoint = new PointF(x + side, y + side);
            ShapeType = ShapeType.Circle;
            Zindex = zIndex;
            ForeColor = foreColor;
            Thickness = thickness;
            BackgroundColor = backgroundColor;
            IsFill = isFill;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Circle" /> class.
        /// </summary>
        public Circle()
        {
            ShapeType = ShapeType.Circle;
        }

#endregion Constructors 

#region Methods (1) 

// Public Methods (1) 

        /// <summary>
        /// Points the in sahpe.
        /// </summary>
        /// <param name="point">The point.</param>
        /// <param name="tolerance">The tolerance.</param>
        /// <returns>
        ///   <c>true</c> if [has point in sahpe] [the specified point]; otherwise, <c>false</c>.
        /// </returns>
        public override bool HasPointInSahpe(PointF point, byte tolerance = 5)
        {
            float width = Math.Abs(EndPoint.X+tolerance - StartPoint.X-tolerance);
            float height = Math.Abs(EndPoint.Y+tolerance - StartPoint.Y-tolerance);
            float diagonal = Math.Max(height, width);
            float raduis = diagonal / 2;
            float dx = Math.Abs(point.X - (X + Width / 2));
            float dy = Math.Abs(point.Y - (Y + height / 2));
            return (dx + dy <= raduis);
        }

#endregion Methods 
    }
}
این کلاس شامل دو سازنده می‌باشد، که در سازنده اول با توجه به نقاط ایتدا و انتهای ترسیم شکل مقدار طول و عرض مستطیل احاطه کننده دایره محاسبه شده و باتوجه به آنها بزرگترین ضلع به عنوان قطر دایره در نظر گرفته می‌شود و EndPoint شکل مورد نظر تعیین می‌شود.

در متد HasPointInShape  با استفاده از فرمول دایره تعیین می‌شود که آیا نقطه پارامتر ورودی متد در داخل دایره واقع شده است یا خیر (جهت انتخاب شکل برای جابجایی یا تغییر اندازه).
در پست‌های بعد به پیاده سازی اینترفیس نرم افزار خواهیم پرداخت.

موفق و موید باشید

در ادامه مطالب قبل:
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 2# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 3#
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 4# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 5# 
مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت چهارم

در این قسمت مدل‌های مربوط به بخش انجمن را تکمیل کرده و همچنین سیستم نظرسنجی را نیز بررسی خواهیم کرد.
همکاران این قسمت: 
سلمان معروفی
سید مجبتی حسینی

مدل پست‌های انجمن

 /// <summary>
    /// Represents The Post of Forum
    /// </summary>
    public class ForumPost : AuditBaseEntity
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="ForumPost"/>
        /// </summary>
        public ForumPost()
        {
            CreatedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets body of this post
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets Count of this post's reports
        /// </summary>
        public virtual int ReportsCount { get; set; }
        /// <summary>
        /// gets or sets information of User-Agent
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// gets or sets rating values 
        /// <remarks>is a complex type</remarks>
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets author's ip address
        /// </summary>
        public virtual string CreatorIp { get; set; }
        /// <summary>
        /// gets or sets status of this post
        /// </summary>
        public virtual ForumPostStatus Status { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets ParentPost of this post
        /// </summary>
        public virtual ForumPost Reply { get; set; }
        /// <summary>
        /// gets or sets ParentPost's Id of this post
        /// </summary>
        public virtual long? ReplyId { get; set; }
        /// <summary>
        /// gets or sets 
        /// </summary>
        public virtual ICollection<ForumPost> Children { get; set; }
        /// <summary>
        /// gets or sets Topic That Associated with this Post
        /// </summary>
        public virtual ForumTopic Topic { get; set; }
        /// <summary>
        /// gets or sets Id of Topic That Associated with this Post
        /// </summary>
        public virtual long TopicId { get; set; }
        /// <summary>
        /// get or sets  Histories of this Post's Updates
        /// </summary>
        public virtual ICollection<ForumPostHistory> Histories { get; set; }
        /// <summary>
        /// gets or sets Forum that this post created in it . used for retrive posts count 
        /// </summary>
        public virtual Forum Forum { get; set; }
        /// <summary>
        /// gets or sets id of Forum that this post created in it . used for retrive posts count 
        /// </summary>
        public virtual long ForumId { get; set; }
        #endregion
    }

 public enum ForumPostStatus 
    {
        /* 0 - approved, 1 - pending, 2 - spam, -1 - trash */
        [Display(Name = "تأیید شده")]
        Approved = 0,
        [Display(Name = "در انتظار بررسی")]
        Pending = 1,
        [Display(Name = "جفنگ")]
        Spam = 2,
        [Display(Name = "زباله دان")]
        Trash = -1
    }

مدل بالا مشخص کننده‌ی پست‌هایی که در پاسخ به تاپیک‌ها ارسال می‌شوند، می‌باشد. ساختار درختی آن به منظور امکان پاسخ به پست‌ها در نظر گرفته شده است. در هر تاپیک چندین پست ارسال می‌شود که اولین پست ارسال شده، همان محتوای اصلی تاپیک می‌باشد. بدین منظور خصوصیت Topic را در مدل بالا تعریف کرده‌ایم. برای این پست‌های ارسالی امکان امتیاز دهی و اخطار دادن نیز خواهیم داشت که به ترتیب خصوصیات Rating و ReportsCount  (بحث شده در مقالات قبل) را در مدل بالا تعریف کرده‌ایم. خصوصیت Status به منظور اعمال مدیریتی در نظر گرفته شده است که از نوع ForumPostStatus می‌باشد و در بالا تعریف آن نیز آمده است.

نکته : خصوصیتی از نوع مدل Forum نیز در مدل بالا تعریف شده است. هدف از آن افزایش سرعت ویرایش خصوصیات ApprovedPostsCount و UnApprovedPostsCount موجود در مدل Forum می‌باشد. در واقع هنگام درج پست جدید یا حذف پستی و یا ... ، لازم است خصوصیات مذکور به روز شوند.

علاوه بر این موارد ، لازم است تاریخچه‌ی تغییرات پست‌های ارسالی را هم نگهداری کرد تا در صورت نیاز به آنها استناد کنیم. از طرفی پست‌های ارسالی را می‌توان چندین بار ویرایش کرد. به همین دلیل خصوصیت Histories را که لیستی از مدل ForumPostHistory می‌باشد، در مدل بالا تعریف کرده‌ایم.

مدل تاریخچه‌ی تغییرات پست

 /// <summary>
    /// Represents History Of Post's Updates
    /// </summary>
    public class ForumPostHistory 
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="ForumPostHistory"/>
        /// </summary>
        public ForumPostHistory()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            CreatedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets Identifier of this history
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets Reason of  update
        /// </summary>
        public virtual string Reason { get; set; }
        /// <summary>
        /// gets or sets DateTime that this record added
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets body of this post
        /// </summary>
        public virtual string Body { get; set; }
        #endregion
        
        #region NavigationProperties
        /// <summary>
        /// gets or sets Post
        /// </summary>
        public virtual ForumPost Post { get; set; }
        /// <summary>
        /// gets or sets Id Of Post
        /// </summary>
        public virtual long PostId { get; set; }
        /// <summary>
        /// gets or sets User that modified this Record
        /// </summary>
        public virtual User Modifier { get; set; }
        /// <summary>
        /// gets or sets if of User that modified this Record
        /// </summary>
        public virtual long ModifierId { get; set; }
        #endregion
    }

اگر خصوصیت ModifyLocked مربوط به مدل ForumPost که آن را از کلاس پایه AuditBaseEntity به ارث برده است، دارای مقدار true باشد، این امکان وجود خواهد داشت تا بتوان پست مورد نظر را ویرایش کرده و اطلاعات قبلی، در قالب یک رکورد در جدول حاصل از مدل بالا ثبت شوند.

  • Reason : دلیل این ویرایش به عمل آماده 
  • Body : محتوای پست یا تاپیک
  • Modifier : کاربر انجام دهنده‌ی این ویرایش 
  • CreatedOn : زمانی که این ویرایش انجام شده است

مدل ردیابی انجمن ها

در خیلی از انجمن‌ها حتما متوجه شده‌اید که لینک برخی از انجمن‌ها یا تاپیک‌های درج شده‌ی در آنها برای شما bold شده نشان داده می‌شود. در واقع، هدف مطلع کردن شما از اینکه در حال حاضر یکسری تاپیک یا پست در انجمن ثبت شده است که شما آنها را مشاهده نکرده‌اید.
 public class ForumTracker
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="ForumTracker"/>
        /// </summary>
        public ForumTracker()
        {
            LastMarkedOn = DateTime.Now;
        }

        #endregion

        #region Properties
        /// <summary>
        /// gets or sets DateTime Of Las Visit by User
        /// </summary>
        public virtual DateTime LastMarkedOn { get; set; }

        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets Forum that Tracked
        /// </summary>
        public virtual Forum Forum { get; set; }
        /// <summary>
        /// gets or sets Id of Forum tath Tracked
        /// </summary>
        public virtual long ForumId { get; set; }
        /// <summary>
        /// gets or sets User that tracked The forum
        /// </summary>
        public virtual User Tracker { get; set; }
        /// <summary>
        /// gets or sets Id Of User that Tracked the forum
        /// </summary>
        public virtual long TrackerId { get; set; }
        #endregion
    }


   public class ForumTopicTracker
      {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="ForumTopicTracker"/>
        /// </summary>
        public ForumTopicTracker()
        {
            LastVisitedOn = DateTime.Now;
        }

        #endregion

        #region Properties
        /// <summary>
        /// gets or sets DateTime Of Las Visit by User
        /// </summary>
        public virtual DateTime LastVisitedOn { get; set; }

        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets topc that Tracked
        /// </summary>
        public virtual ForumTopic Topic { get; set; }
        /// <summary>
        /// gets or sets Id of topic that Tracked
        /// </summary>
        public virtual long TopicId { get; set; }
        /// <summary>
        /// gets or sets User that tracked The topic
        /// </summary>
        public virtual User Tracker { get; set; }
        /// <summary>
        /// gets or sets Id Of User that Tracked the topic
        /// </summary>
        public virtual long TrackerId { get; set; }
        /// <summary>
        /// gets or sets Forum 
        /// </summary>
        public virtual Forum Forum { get; set; }
        /// <summary>
        /// gets or sets Identifier of Forum . used for delete 
        /// </summary>
        public virtual long ForumId { get; set; }

        #endregion
      }
در سیستم، برای کاربران احراز هویت شده، این امکان را مهیا ساخته‌ایم تا انجمن‌ها و تایپک‌هایی که پست جدید ارسال شده دارند و توسط کاربر خوانده نشده است، به نحوی متمایز نشان داده شوند. 
برای این منظور از دو مدل پیاده سازی شده‌ی در بالا و یک خصوصیت از نوع تاریخ تحت عنوان LastMarkedOn در مدل User، استفاده خواهیم کرد. در واقع از LastMarkedOn مدل User، برای نگه داری آخرین تاریخی استفاده می‌شود که کاربر تمام انجمن‌ها را خوانده شده علامت گذاری کرده است. در این صورت می‌توان تمام رکورد‌های ذخیره شده‌ی در جداول ForumTrackers و ForumTopicTrackers را که قبل از این تاریخ هستند، حذف کرد. از LastMarkedOn مدل ForumTracker هم برای نگهداری تاریخی استفاده می‌شود که یک انجمن خاص را خوانده شده علامت گذاری کرده است و همچنین می‌توان تمام رکورد‌های مربوط به آن انجمن را در جدول ForumTopicTrackers حذف کرد.

از مدل ForumTopicTracker هم برای مشخص کردن اینکه کاربر کدام تاپیک را و در چه تاریخی آخرین بار مشاهده کرده است، کمک می‌گیریم. برای این منظور از خصوصیت LastVisitedOn استفاده می‌شود.

البته نیاز است هنگام واکشی انجمن‌ها و تاپیک‌ها، یکسری بررسی‌هایی را بر اساس این جداول انجام داد که تشریح این بررسی‌ها را  قصد دارم هنگام پیاده سازی سیستم انجام دهم. 

این قسمت از کار کمی پیچیده است و برای خودم نیز چالش داشت. سعی کردم انجمن‌های سورس باز PHP را بررسی کنم تا در نهایت به تحلیل بالا دست یافتم. مدل‌های ارائه شده انجمن تا این قسمت، نیازهای مورد نظر ما را برآورده خواهند کرد.

مدل سیستم نظرسنجی

public class Poll : BaseContent
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="Poll"/>
        /// </summary>
        public Poll()
        {
            Rating = new Rating();
            PublishedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or set Date that this Poll will Expire
        /// </summary>
        public virtual DateTime? ExpireOn { get; set; }
        /// <summary>
        ///indicating this poll allow to select multi item
        /// </summary>
        public virtual bool IsMultiSelect { get; set; }
        /// <summary>
        /// gets or sets Count of this poll's votes 
        /// </summary>
        public virtual long VotesCount { get; set; }
        /// <summary>
        /// indicate this Poll is approved by admin if Poll.Moderate==true
        /// </summary>
        public virtual bool IsApproved { get; set; }

        #endregion

        #region NavigationProperties
        /// <summary>
        /// get or set comments of this poll
        /// </summary>
        public virtual ICollection<PollComment> Comments { get; set; }
        /// <summary>
        /// get or set Options Of Poll For selection
        /// </summary>
        public virtual ICollection<PollOption> Options { get; set; }
        /// <summary>
        /// get or set Users List That vote for this poll
        /// </summary>
        public virtual ICollection<User> Voters { get; set; }
        #endregion
    }
مدل بالا مشخص کننده‌ی نظرسنجی‌های سیستم ما می‌باشد. این مدل نیز از کلاس پایه مطرح شده در مقاله اول ارث بری کرده است و علاوه بر آن یکسری خصوصیت دیگر را به شرح زیر دارد:
  • ExpireOn : زمان اتمام فرصت رای دهی که اگر نال باشد در آن صورت زمان انقضا نخواهد داشت.
  • IsMultiSelect : اگر انتخاب چندگزینه‌ای مجاز باشد، این خصوصیت، با مقدار true مقدار دهی می‌شود.
  • VotesCount : به منظور افزایش کارآیی در نظر گرفته شده است و تعداد کل رای‌های داده شده‌ی به نظرسنجی را در بر می‌گیرد.
  • Voters : برای جلوگیری از رای دهی چند باره‌ی کاربر به یک نظرسنجی، یک ارتباط چند به چند بین کاربر و نظرسنجی برقرار کرده‌ایم. هر کاربر به چند نظر سنجی می‌تواند پاسخ دهد و به هر نظرسنجی توسط چندین کاربر رای داده می‌شود.
  • PollOptions : هر نظر سنجی تعدادی گزینه‌ی انتخابی هم خواهد داشت که برای همین منظور و اعمال ارتباط یک به چند بین نظرسنجی و گزینه‌های انتخابی، لیستی از PollOption را در مدل بالا تعریف کرده‌ایم.

مدل گزینه‌های نظرسنجی

 public class PollOption
    {
        #region Properties
        /// <summary>
        /// gets or sets identifier of this polloption
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets Title of this polloption
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets count of votes 
        /// </summary>
        public virtual long VotesCount { get; set; }
        /// <summary>
        /// gets or sets Description of this Option for more details
        /// </summary>
        public virtual string Description { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets the poll that assosiated with this Polloption
        /// </summary>
        public virtual Poll Poll { get; set; }
        /// <summary>
        /// gets or sets the id of poll that assosiated with this Polloption
        /// </summary>
        public virtual long PollId { get; set; }
        #endregion
    }
مدل بالا نشان دهنده‌ی گزینه‌های انتخابی در هر نظر سنجی می‌باشد. خصوصیت Poll و به دنبال آن PollId به منظور اعمال ارتباط یک به چند بین نظرسنجی و گزینه‌ها در نظر گرفته شده‌اند. 
  • Title: عنوان گزینه‌ی مورد نظر
  • Description: توضیح بیشتر برای گزینه‌ی مورد نظر
  • VotesCount: تعداد باری که یک گزینه در نظر سنجی انتخاب شده است.
در این سیستم نیازی نیست که بدانیم چه کاربرانی در یک نظر سنجی کدام گزینه را انتخاب کرده‌اند و لذا مدل بالا برای کار ما کافی است.

مدل نظرات سیستم نظرسنجی

 public class PollComment : BaseComment
    {
        #region Ctor
        public PollComment()
        {
            CreatedOn = DateTime.Now;
            Rating = new Rating();
        }
        #endregion

        #region NavigationProperties

        /// <summary>
        /// gets or sets body of blog poll's comment
        /// </summary>
        public virtual long? ReplyId { get; set; }
        /// <summary>
        /// gets or sets body of blog poll's comment
        /// </summary>
        public virtual PollComment Reply { get; set; }
        /// <summary>
        /// gets or sets body of blog poll's comment
        /// </summary>
        public virtual ICollection<PollComment> Children { get; set; }
        /// <summary>
        /// gets or sets poll that this comment sent to it
        /// </summary>
        public virtual Poll Poll { get; set; }
        /// <summary>
        /// gets or sets poll'Id that this comment sent to it
        /// </summary>
        public virtual long PollId { get; set; }
        #endregion
    }
مدل بالا نیز از کلاس پایه‌ی BaseComment مورد بحث در مقاله‌ی اول  ارث بری کرده است و ساختار درختی آن نیز مشخص است و همچنین یک ارتباط یک به چند بین نظرسنجی‌ها و نظرات وجود خواهد داشت که برای این منظور خصوصیت Poll را در مدل بالا تعریف کرده‌ایم.

در مقاله‌ی بعد به بررسی سیستم پیام رسانی و همچنین بخشی از سیستم تحت عنوان Collections (امکان ساخت گروه‌های شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسی‌های مختلف) خواهیم پرداخت.

نتیجه تا این قسمت


اشتراک‌ها
نگاهی به تاریخچه‌ی ASP.NET - قسمت دوم
Part 1 took an overview of the initial design of ASP.NET and how Microsoft reacted to the various changes in webdev. In Part II, we will now look at how those changes influenced the development of ASP.NET MVC and ended up transforming ASP.NET into a much more flexible framework composed of multiple libraries that solved different problems.
نگاهی به تاریخچه‌ی ASP.NET - قسمت دوم
مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت ششم - کار با منابع محافظت شده‌ی سمت سرور
پس از تکمیل کنترل دسترسی‌ها به قسمت‌های مختلف برنامه بر اساس نقش‌های انتسابی به کاربر وارد شده‌ی به سیستم، اکنون نوبت به کار با سرور و دریافت اطلاعات از کنترلرهای محافظت شده‌ی آن است.



افزودن کامپوننت دسترسی به منابع محافظت شده، به ماژول Dashboard

در اینجا قصد داریم صفحه‌ای را به برنامه اضافه کنیم تا در آن بتوان اطلاعات کنترلرهای محافظت شده‌ی سمت سرور، مانند MyProtectedAdminApiController (تنها قابل دسترسی توسط کاربرانی دارای نقش Admin) و MyProtectedApiController (قابل دسترسی برای عموم کاربران وارد شده‌ی به سیستم) را دریافت و نمایش دهیم. به همین جهت کامپوننت جدیدی را به ماژول Dashboard اضافه می‌کنیم:
 >ng g c Dashboard/CallProtectedApi
سپس به فایل dashboard-routing.module.ts ایجاد شده مراجعه کرده و مسیریابی کامپوننت جدید ProtectedPage را اضافه می‌کنیم:
import { CallProtectedApiComponent } from "./call-protected-api/call-protected-api.component";

const routes: Routes = [
  {
    path: "callProtectedApi",
    component: CallProtectedApiComponent,
    data: {
      permission: {
        permittedRoles: ["Admin", "User"],
        deniedRoles: null
      } as AuthGuardPermission
    },
    canActivate: [AuthGuard]
  }
];
توضیحات AuthGuard و AuthGuardPermission را در قسمت قبل مطالعه کردید. در اینجا هدف این است که تنها کاربران دارای نقش‌های Admin و یا User قادر به دسترسی به این مسیر باشند.
لینکی را به این صفحه نیز در فایل header.component.html به صورت ذیل اضافه خواهیم کرد تا فقط توسط کاربران وارد شده‌ی به سیستم (isLoggedIn) قابل مشاهده باشد:
<li *ngIf="isLoggedIn" routerLinkActive="active">
        <a [routerLink]="['/callProtectedApi']">‍‍Call Protected Api</a>
</li>


نمایش و یا مخفی کردن قسمت‌های مختلف صفحه بر اساس نقش‌های کاربر وارد شده‌ی به سیستم

در ادامه می‌خواهیم دو دکمه را بر روی صفحه قرار دهیم تا اطلاعات کنترلرهای محافظت شده‌ی سمت سرور را بازگشت دهند. دکمه‌ی اول قرار است تنها برای کاربر Admin قابل مشاهده باشد و دکمه‌ی دوم توسط کاربری با نقش‌های Admin و یا User.
به همین جهت call-protected-api.component.ts را به صورت ذیل تغییر می‌دهیم:
import { Component, OnInit } from "@angular/core";
import { AuthService } from "../../core/services/auth.service";

@Component({
  selector: "app-call-protected-api",
  templateUrl: "./call-protected-api.component.html",
  styleUrls: ["./call-protected-api.component.css"]
})
export class CallProtectedApiComponent implements OnInit {

  isAdmin = false;
  isUser = false;
  result: any;

  constructor(private authService: AuthService) { }

  ngOnInit() {
    this.isAdmin = this.authService.isAuthUserInRole("Admin");
    this.isUser = this.authService.isAuthUserInRole("User");
  }

  callMyProtectedAdminApiController() {
  }

  callMyProtectedApiController() {
  }
}
در اینجا دو خاصیت عمومی isAdmin و isUser، در اختیار قالب این کامپوننت قرار گرفته‌اند. مقدار دهی آن‌ها نیز توسط متد isAuthUserInRole که در قسمت قبل توسعه دادیم، انجام می‌شود. اکنون که این دو خاصیت مقدار دهی شده‌اند، می‌توان از آن‌ها به کمک یک ngIf، به صورت ذیل در قالب call-protected-api.component.html جهت مخفی کردن و یا نمایش قسمت‌های مختلف صفحه استفاده کرد:
<button *ngIf="isAdmin" (click)="callMyProtectedAdminApiController()">
  Call Protected Admin API [Authorize(Roles = "Admin")]
</button>

<button *ngIf="isAdmin || isUser" (click)="callMyProtectedApiController()">
  Call Protected API ([Authorize])
</button>

<div *ngIf="result">
  <pre>{{result | json}}</pre>
</div>


دریافت اطلاعات از کنترلرهای محافظت شده‌ی سمت سرور

برای دریافت اطلاعات از کنترلرهای محافظت شده، باید در قسمتی که HttpClient درخواست خود را به سرور ارسال می‌کند، هدر مخصوص Authorization را که شامل توکن دسترسی است، به سمت سرور ارسال کرد. این هدر ویژه را به صورت ذیل می‌توان در AuthService تولید نمود:
  getBearerAuthHeader(): HttpHeaders {
    return new HttpHeaders({
      "Content-Type": "application/json",
      "Authorization": `Bearer ${this.getRawAuthToken(AuthTokenType.AccessToken)}`
    });
  }

روش دوم انجام اینکار که مرسوم‌تر است، اضافه کردن خودکار این هدر به تمام درخواست‌های ارسالی به سمت سرور است. برای اینکار باید یک HttpInterceptor را تهیه کرد. به همین منظور فایل جدید app\core\services\auth.interceptor.ts را به برنامه اضافه کرده و به صورت ذیل تکمیل می‌کنیم:
import { Injectable } from "@angular/core";
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from "@angular/common/http";
import { Observable } from "rxjs/Observable";

import { AuthService, AuthTokenType } from "./auth.service";

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private authService: AuthService) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    const accessToken = this.authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
    }

    return next.handle(request);
  }
}
در اینجا یک clone از درخواست جاری ایجاد شده و سپس به headers آن، یک هدر جدید Authorization که به همراه توکن دسترسی است، اضافه خواهد شد.
به این ترتیب دیگری نیازی نیست تا به ازای هر درخواست و هر قسمتی از برنامه، این هدر را به صورت دستی تنظیم کرد و اضافه شدن آن پس از تنظیم ذیل، به صورت خودکار انجام می‌شود:
import { HTTP_INTERCEPTORS } from "@angular/common/http";

import { AuthInterceptor } from "./services/auth.interceptor";

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ]
})
export class CoreModule {}
در اینجا نحوه‌ی معرفی این HttpInterceptor جدید را به قسمت providers مخصوص CoreModule مشاهده می‌کنید.

در این حالت اگر برنامه را اجرا کنید، خطای ذیل را در کنسول توسعه‌دهنده‌های مرورگر مشاهده خواهید کرد:
compiler.js:19514 Uncaught Error: Provider parse errors:
Cannot instantiate cyclic dependency! InjectionToken_HTTP_INTERCEPTORS ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1
در سازنده‌ی کلاس سرویس AuthInterceptor، سرویس Auth تزریق شده‌است که این سرویس نیز دارای HttpClient تزریق شده‌ی در سازنده‌ی آن است. به همین جهت Angular تصور می‌کند که ممکن است در اینجا یک بازگشت بی‌نهایت بین این interceptor و سرویس Auth رخ‌دهد. اما از آنجائیکه ما هیچکدام از متدهایی را که با HttpClient کار می‌کنند، در اینجا فراخوانی نمی‌کنیم و تنها کاربرد سرویس Auth، دریافت توکن دسترسی است، این مشکل را می‌توان به صورت ذیل برطرف کرد:
import { Injector } from "@angular/core";

  constructor(private injector: Injector) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
ابتدا سرویس Injector را به سازنده‌ی کلاس AuthInterceptor تزریق می‌کنیم و سپس توسط متد get آن، سرویس Auth را درخواست خواهیم کرد (بجای تزریق مستقیم آن در سازنده‌ی کلاس):
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private injector: Injector) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
    const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
    }

    return next.handle(request);
  }
}


تکمیل متدهای دریافت اطلاعات از کنترلرهای محافظت شده‌ی سمت سرور

اکنون پس از افزودن AuthInterceptor، می‌توان متدهای CallProtectedApiComponent را به صورت ذیل تکمیل کرد. ابتدا سرویس‌های Auth ،HttpClient و همچنین تنظیمات آغازین برنامه را به سازنده‌ی CallProtectedApiComponent تزریق می‌کنیم:
  constructor(
    private authService: AuthService,
    private httpClient: HttpClient,
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
  ) { }
سپس متدهای httpClient.get و یا هر نوع متد مشابه دیگری را به صورت معمولی فراخوانی خواهیم کرد:
  callMyProtectedAdminApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedAdminApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }

  callMyProtectedApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }

در این حالت اگر برنامه را اجرا کنید، افزوده شدن خودکار هدر مخصوص Authorization:Bearer را در درخواست ارسالی به سمت سرور، مشاهده خواهید کرد:



مدیریت خودکار خطاهای عدم دسترسی ارسال شده‌ی از سمت سرور

ممکن است کاربری درخواستی را به منبع محافظت شده‌ای ارسال کند که به آن دسترسی ندارد. در AuthInterceptor تعریف شده می‌توان به وضعیت این خطا، دسترسی یافت و سپس کاربر را به صفحه‌ی accessDenied که در قسمت قبل ایجاد کردیم، به صورت خودکار هدایت کرد:
    return next.handle(request)
      .catch((error: any, caught: Observable<HttpEvent<any>>) => {
        if (error.status === 401 || error.status === 403) {
          this.router.navigate(["/accessDenied"]);
        }
        return Observable.throw(error);
      });
در اینجا ابتدا نیاز است سرویس Router، به سازنده‌ی کلاس تزریق شود و سپس متد catch درخواست پردازش شده، به صورت فوق جهت عکس العمل نشان دادن به وضعیت‌های 401 و یا 403 و هدایت کاربر به مسیر accessDenied تغییر کند:
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(
    private injector: Injector,
    private router: Router) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
    const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
      return next.handle(request)
        .catch((error: any, caught: Observable<HttpEvent<any>>) => {
          if (error.status === 401 || error.status === 403) {
            this.router.navigate(["/accessDenied"]);
          }
          return Observable.throw(error);
        });
    } else {
      // login page
      return next.handle(request);
    }
  }
}
برای آزمایش آن، یک کنترلر سمت سرور جدید را با نقش Editor اضافه می‌کنیم:
using ASPNETCore2JwtAuthentication.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;

namespace ASPNETCore2JwtAuthentication.WebApp.Controllers
{
    [Route("api/[controller]")]
    [EnableCors("CorsPolicy")]
    [Authorize(Policy = CustomRoles.Editor)]
    public class MyProtectedEditorsApiController : Controller
    {
        public IActionResult Get()
        {
            return Ok(new
            {
                Id = 1,
                Title = "Hello from My Protected Editors Controller! [Authorize(Policy = CustomRoles.Editor)]",
                Username = this.User.Identity.Name
            });
        }
    }
}
و برای فراخوانی سمت کلاینت آن در CallProtectedApiComponent خواهیم داشت:
  callMyProtectedEditorsApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedEditorsApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }
چون این نقش جدید به کاربر جاری انتساب داده نشده‌است (جزو اطلاعات سمت سرور او نیست)، اگر آن‌را توسط متد فوق فراخوانی کند، خطای 403 را دریافت کرده و به صورت خودکار به مسیر accessDenied هدایت می‌شود:



نکته‌ی مهم: نیاز به دائمی کردن کلیدهای رمزنگاری سمت سرور

اگر برنامه‌ی سمت سرور ما که توکن‌ها را اعتبارسنجی می‌کند، ری‌استارت شود، چون قسمتی از کلیدهای رمزگشایی اطلاعات آن با اینکار مجددا تولید خواهند شد، حتی با فرض لاگین بودن شخص در سمت کلاینت، توکن‌های فعلی او برگشت خواهند خورد و از مرحله‌ی تعیین اعتبار رد نمی‌شوند. در این حالت کاربر خطای 401 را دریافت می‌کند. بنابراین پیاده سازی مطلب «غیرمعتبر شدن کوکی‌های برنامه‌های ASP.NET Core هاست شده‌ی در IIS پس از ری‌استارت آن» را فراموش نکنید.



کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود. 
نظرات مطالب
ایجاد لینک با یک تصویر بوسیله Html Helper
Compiler Error Message: CS0103: The name 'workout' does not exist in the current context  
@using ActionImage.Extentions;
@Html.ActionImage("Workout", "Delete", new { id = @workout.Id }, "/images/bluetrash.png", "Delete", new { id="deletebutton"}) 
اشتراک‌ها
مشخصات یک ایمیل خوب

Email communication is not my favorite but since I can’t avoid it, I am trying to compose messages in a way that I think it makes it easier for both me and the recipient:
- to quickly address what is being communicated
- avoid misunderstandings
- save time
 

مشخصات یک ایمیل خوب
مطالب
C# 6 - The nameof Operator
یکی دیگر از قابلیت‌های جذاب نسخه‌ی جدید سی‌شارپ، عملگر nameof است. هدف اصلی آن ارائه کدهایی با قابلیت Refactoring بهتر است؛ زیرا به جای نوشتن نام فیلدها و یا متدها در صورت نیاز به صورت hard-coded، می‌توانیم از این عملگر استفاده کنیم. به عنوان مثال در زمان صدور استثناءیی از نوع ArgumentNullException باید نام آرگومان را به سازنده‌ی این کلاس پاس دهیم. متاسفانه یکی از مشکلاتی که با رشته‌ها در حالت کلی وجود دارد این است که امکان دیباگ در زمان کامپایل را از دست خواهیم داد و با تغییر هر المنت، تغییرات به صورت خودکار به رشته پاس داده شده، به سازنده‌ی کلاس ArgumentNullException اعمال نخواهد شد:
public static void DoWork(string name)
{
       if (name == null)
       {
           throw new ArgumentNullException("name");
       }
}
اما با استفاده از عملگر nameof، کد امن‌تری را خواهیم داشت؛ زیرا همیشه نام واقعی آرگومان به سازنده‌ی کلاس ArgumentNullException پاس داده می‌شود:
public static void DoWork(string name)
{
       if (name == null)
       {
           throw new ArgumentNullException(nameof(name));
       }
}
اگر ReSharper را نصب کرده باشید، به شما پیشنهاد می‌دهد که از nameof به جای یک رشته‌ی جادویی (magic string) استفاده نمائید:


یک مثال دیگر می‌تواند در زمان فراخوانی رخدادهای مربوط به OnPropertyChanged باشد. در اینجا باید نام خصوصیتی را که تغییر یافته است، به آن پاس دهیم: 
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged("Name");
            }
        }
اما با کمک عملگر nameof می‌توانیم قسمت فراخوانی متد OnPropertyChanged را به اینصورت نیز بازنویسی کنیم:
OnPropertyChanged(nameof(Name));
ممکن است عنوان کنید قبلاً در سی‌شارپ 5 هم می‌توانستیم از ویژگی [CallerMemberName] استفاده کنیم، پس دیگر نیازی به استفاده از عملگر nameof نخواهد بود. اما تفاوت کلیدی این است که CallerMemberName در زمان اجرا نام فیلد فراخوان را دریافت میکند (run time)، در حالیکه با استفاده از عملگر nameof می‌توانید در زمان کامپایل به نام فیلد دسترسی داشته باشید (compile time).

محدودیت‌های عملگر nameof
این عملگر حالت‌هایی را که مشاهده می‌کنید، فعلاً پشتیبانی نخواهد کرد:
nameof(f()); // where f is a method - you could use nameof(f) instead
nameof(c._Age); // where c is a different class and _Age is private. Nameof can't break accessor rules.
nameof(List<>); // List<> isn't valid C# anyway, so this won't work
nameof(default(List<int>)); // default returns an instance, not a member
nameof(int); // int is a keyword, not a member- you could do nameof(Int32)
nameof(x[2]); // returns an instance using an indexer, so not a member
nameof("hello"); // a string isn't a member
nameof(1 + 2); // an int isn't a member
برای آزمایش عملگر nameof می‌توانیم یک تست را در حالت‌های زیر بنویسیم:


همانطور که مشاهده می‌کنید، همه‌ی حالت‌های فوق با موفقیت پاس شده‌اند.
مطالب
پردازش داده‌های جغرافیایی به کمک SQL Server و Entity framework
پشتیبانی SQL Server از Spatial data

از SQL Server 2008 به بعد، نوع داده جدیدی به نام geography به نوع‌های قابل تعریف ستون‌ها اضافه شده‌است. در این نوع ستون‌ها می‌توان طول و عرض جغرافیایی یک نقطه را ذخیره کرد و سپس به کمک توابع توکاری از آن‌ها کوئری گرفت.


در اینجا نمونه‌ای از نحوه‌ی تعریف و همچنین مقدار دهی این نوع ستون‌ها را مشاهده می‌کنید:
 CREATE TABLE [Geo](
[id] [int] IDENTITY(1,1) NOT NULL,
[Location] [geography] NULL
)

 insert into Geo( Location , long, lat ) values
( geography::STGeomFromText ('POINT(-121.527200 45.712113)', 4326))
متد geography::STGeoFromText یک SQL CLR function است. این متد در مثال فوق، مختصات یک نقطه را دریافت کرده‌است. همچنین نیاز دارد بداند که این نقطه توسط چه نوع سیستم مختصاتی ارائه می‌شود. عدد 4326 در اینجا یک SRID یا Spatial Reference System Identifier استاندارد است. برای نمونه اطلاعات ارائه شده توسط Google و یا Bing توسط این استاندارد ارائه می‌شوند.
در اینجا متدهای توکار دیگری مانند geography::STDistance برای یافتن فاصله مستقیم بین نقاط نیز ارائه شد‌ه‌اند. خروجی آن بر حسب متر است.


پشتیبانی از Spatial Data در Entity framework

پشتیبانی از نوع مخصوص geography، در EF 5 توسط نوع داده‌ای DbGeography ارائه شد. این نوع داده‌ای immutable است. به این معنا که پس از نمونه سازی، دیگر مقدار آن قابل تغییر نیست.
در اینجا برای نمونه مدلی را مشاهده می‌کنید که از نوع داده‌ای DbGeography استفاده می‌کند:
using System.Data.Entity.Spatial;

namespace EFGeoTests.Models
{
    public class GeoLocation
    {
        public int Id { get; set; }
        public DbGeography Location { get; set; }
        public string Name { get; set; }
        public string Type { get; set; }

        public override string ToString()
        {
            return string.Format("Name:{0}, Location:{1}", Name, Location);
        }
    }
}
به همراه یک Context، تا کلاس GeoLocation در معرض دید EF قرار گیرد:
using System;
using System.Data.Entity;
using EFGeoTests.Models;

namespace EFGeoTests.Config
{
    public class MyContext : DbContext
    {
        public DbSet<GeoLocation> GeoLocations { get; set; }

        public MyContext()
            : base("Connection1")
        {
            this.Database.Log = sql => Console.Write(sql);
        }
    }
}
برای مقدار دهی خاصیت Location از نوع DbGeography می‌توان از متد ذیل استفاده کرد که بسیار شبیه به متد geography::STGeoFromText عمل می‌کند:
   private static DbGeography createPoint(double longitude, double latitude,  int coordinateSystemId = 4326)
  {
       var text = string.Format(CultureInfo.InvariantCulture.NumberFormat,"POINT({0} {1})", longitude, latitude);
       return DbGeography.PointFromText(text, coordinateSystemId);
  }


تهیه منبع داده‌ی جغرافیایی

برای تدارک یک مثال واقعی جغرافیایی، نیاز به اطلاعاتی دقیق داریم. این نوع اطلاعات عموما توسط یک سری فایل مخصوص به نام Shapefiles که حاوی اطلاعات برداری جغرافیایی هستند ارائه می‌شوند. برای نمونه اطلاعات جغرافیایی به روز ایران را از آدرس ذیل می‌توانید دریافت کنید:
http://download.geofabrik.de/asia/iran.html
http://download.geofabrik.de/asia/iran-latest.shp.zip

پس از دریافت این فایل، به تعدادی فایل با پسوندهای shp، shx و dbf خواهیم رسید.
فایل‌های shp بیانگر فرمت اشکال ذخیره شده هستند. فایل‌های shx یک سری ایندکس بوده و فایل‌های dbf از نوع بانک اطلاعاتی dBase IV می‌باشند.
همچنین اگر فایل‌های prj را باز کنید، یک چنین اطلاعاتی در آن موجودند:
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
نکته‌ی مهمی که در اینجا باید مدنظر داشت، استاندارد GCS_WGS_1984 آن است. این استاندارد معادل است با استاندارد EPSG 4326. عدد 4326 آن جهت ثبت این اطلاعات در یک بانک اطلاعاتی SQL Server حائز اهمیت است (پارامتر coordinateSystemId در متد createPoint) و ممکن است از هر فایلی به فایل دیگر متفاوت باشد.



خواند‌ن فایل‌های shp در دات نت

پس از دریافت فایل‌های shp و بانک‌های اطلاعاتی مرتبط با اطلاعات جغرافیایی ایران، اکنون نوبت به پردازش این فایل‌های مخصوص با فرمت بانک اطلاعاتی فاکس پرو مانند، رسیده‌است. برای این منظور می‌توان از پروژه‌ی سورس باز ذیل استفاده کرد:

این پروژه در خواندن فایل‌های shp بدون نقص عمل می‌کند اما توانایی خواندن نام‌های فارسی وارد شده در این نوع بانک‌های اطلاعاتی را ندارد. برای رفع این مشکل، سورس آن را از Codeplex دریافت کنید. سپس فایل Shapefile.cs را گشوده و ابتدای خاصیت Current آن‌را به نحو ذیل تغییر دهید:
        /// <summary>
        /// Gets the current shape in the collection
        /// </summary>
        public Shape Current
        {
            get 
            {
                if (_disposed) throw new ObjectDisposedException("Shapefile");
                if (!_opened) throw new InvalidOperationException("Shapefile not open.");
               
                // get the metadata
                StringDictionary metadata = null;
                if (!RawMetadataOnly)
                {
                    metadata = new StringDictionary();
                    for (int i = 0; i < _dbReader.FieldCount; i++)
                    {
                        string value = _dbReader.GetValue(i).ToString();
                        if (_dbReader.GetDataTypeName(i) == "DBTYPE_WVARCHAR")
                        {
                            // برای نمایش متون فارسی نیاز است
                            value = Encoding.UTF8.GetString(Encoding.GetEncoding(720).GetBytes(value));
                        }
                        metadata.Add(_dbReader.GetName(i),
                            value);
                    }
                }
در اینجا فقط سطر استفاده از Encoding خاصی با شماره 720 و تبدیل آن به UTF8 اضافه شده‌است. پس از آن بدون مشکل می‌توان برچسب‌های فارسی را از فایل‌های dBase IV این نوع بانک‌های اطلاعاتی استخراج کرد (اصلاح شده‌ی آن در فایل پیوست مطلب موجود است).
using System.Collections.Generic;
using System.Linq;
using Catfood.Shapefile;

namespace EFGeoTests
{
    public class MapPoint
    {
        public Dictionary<string, string> Metadata { set; get; }
        public double X { set; get; }
        public double Y { set; get; }
    }

    public static class ShapeReader
    {
        public static IList<MapPoint> ReadShapeFile(string path)
        {
            var results = new List<MapPoint>();

            using (var shapefile = new Shapefile(path))
            {
                foreach (var shape in shapefile)
                {
                    if (shape.Type != ShapeType.Point)
                        continue;

                    var shapePoint = shape as ShapePoint;
                    if (shapePoint == null)
                        continue;


                     var metadataNames = shape.GetMetadataNames();
                    if(!metadataNames.Any())
                        continue;

                    var metadata = new Dictionary<string, string>();
                    foreach (var metadataName in metadataNames)
                    {
                        metadata.Add(metadataName,shape.GetMetadata(metadataName));
                    }

                    results.Add(new MapPoint
                    {
                        Metadata = metadata,
                        X = shapePoint.Point.X,
                        Y = shapePoint.Point.Y
                    });
                }
            }

            return results;
        }
    }
}
در کدهای فوق به کمک کتابخانه‌ی C# Esri Shapefile Reader، اطلاعات نقاط بانک اطلاعاتی shape files را خوانده و به صورت لیست‌هایی از MapPoint بازگشت می‌دهیم. نکته‌ی مهم آن، Metadata است که از هر فایلی به فایل دیگر می‌توان متفاوت باشد. به همین جهت این اطلاعات را به شکل ویژگی‌های key/value در این نوع بانک‌های اطلاعاتی ذخیره می‌کنند.


افزودن اطلاعات جغرافیایی به بانک اطلاعاتی SQL Server به کمک Entity framework

فایل places.shp را در مجموعه فایل‌هایی که در ابتدای بحث عنوان شدند، می‌توانید مشاهده کنید. قصد داریم اطلاعات نقاط آن‌را به مدل GeoLocation انتساب داده و سپس ذخیره کنیم:
            var points = ShapeReader.ReadShapeFile("IranShapeFiles\\places.shp");
            using (var context = new MyContext())
            {
                context.Configuration.AutoDetectChangesEnabled = false;
                context.Configuration.ProxyCreationEnabled = false;
                context.Configuration.ValidateOnSaveEnabled = false;

                if (context.GeoLocations.Any())
                    return;

                foreach (var point in points)
                {
                    context.GeoLocations.Add(new GeoLocation
                    {
                        Name = point.Metadata["name"],
                        Type = point.Metadata["type"],
                        Location = createPoint(point.X, point.Y)
                    });
                }

                context.SaveChanges();
            }
تعریف متد createPoint را که بر اساس X و Y نقاط، معادل قابل پذیرش آن‌را جهت SQL Server تهیه می‌کند، در ابتدای بحث مشاهده کردید.
در فایل‌های مرتبط با places.shp، متادیتا name، معادل نام شهرهای ایران است و type آن بیانگر شهر، روستا و امثال آن می‌باشد.
پس از اینکه اطلاعات مکان‌های ایران، در SQL Server ذخیره شدند، نمایش بصری آن‌ها را در management studio نیز می‌توان مشاهده کرد:



کوئری گرفتن از اطلاعات جغرافیایی

فرض کنید می‌خواهیم مکان‌هایی را با فاصله کمتر از 5 کیلومتر از تهران پیدا کنیم:
            var tehran = createPoint(51.4179604, 35.6884243);

            using (var context = new MyContext())
            {
                // find any locations within 5 kilometers ordered by distance
                var locations = context.GeoLocations
                    .Where(loc => loc.Location.Distance(tehran) < 5000)
                    .OrderBy(loc => loc.Location.Distance(tehran))
                    .ToList();

                foreach (var location in locations)
                {
                    Console.WriteLine(location.Name);
                }
            }
همانطور که پیشتر نیز عنوان شد، متد Distance بر اساس متر کار می‌کند. به همین جهت برای تعریف 5 کیلومتر به نحو فوق عمل شده‌است. همچنین نحوه‌ی مرتب سازی اطلاعات نیز بر اساس فاصله از یک مکان مشخص صورت گرفته‌است.
و یا اگر بخواهیم دقیقا بر اساس مختصات یک نقطه، مکانی را بیابیم، می‌توان از متد SpatialEquals استفاده کرد:
            var tehran = createPoint(51.4179604, 35.6884243);
            using (var context = new MyContext())
            {
                // find any locations within 5 kilometers ordered by distance
                var tehranLocation = context.GeoLocations.FirstOrDefault(loc => loc.Location.SpatialEquals(tehran));
                if (tehranLocation != null)
                {
                    Console.WriteLine(tehranLocation.Type);
                }
            }

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
EFGeoTests.zip