خاصیتهای جدید VirtualizingPanel در دات نت 4.5
تمام کنترلهای مشتق شدهی از ListBox مانند ListView و امثال آن، در WPF 4.5 امکان تنظیم یک چنین خواصی را یافتهاند:
<ListBox ItemsSource="{Binding Persons}" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.ScrollUnit="Pixel" VirtualizingPanel.IsVirtualizingWhenGrouping="True" VirtualizingPanel.CacheLength="100" VirtualizingPanel.CacheLengthUnit="Pixel"/>
در اینجا میتوان دو خاصیت CacheLength و CacheLengthUnit را مقدار دهی کرد. مقدار پیش فرض CacheLength مساوی 1.1 است. CacheLengthUnit میتواند یکی از مقادیر Pixel, Item یا Page را بپذیرد. هر Page در اینجا بر اساس اندازهی viewport یا قسمت نمایان لیست، تعریف میشود. مقدار پیش فرض آن نیز Item است.
همچنین در اینجا میتوان ScrollUnit را نیز تنظیم کرد که مقادیر Item یا Pixel را میپذیرد. حالت پیش فرض آن Item است؛ به این معنا که اگر ردیفی کاملا در viewport جای نگرفت، نمایش داده نمیشود. این مورد را با تنظیم مقدار ScrollUnit به Pixel میتوان بهبود بخشید.
IsVirtualizingWhenGrouping سبب فعال سازی مجازی سازی حتی در حالت گروه بندی اطلاعات میگردد. به صورت پیش فرض اگر گروه بندی فعال شود، دیگر مجازی سازی رخ نخواهد داد.
فعال سازی قابلیتهای دات نت 4.5 در برنامههای WPF 4
اگر برنامهی WPF 4 خود را فعلا قصد ندارید به دات نت 4.5 ارتقاء دهید، با توجه به اینکه اگر کاربر دات نت 4.5 را نصب کرده باشد، برنامهی شما به صورت خودکار همانند یک برنامهی WPF 4.5 رفتار میکند (دات نت 4.5 جایگزین دات نت 4 میشود)، میتوان با اندکی Reflection این قابلیتها را در صورت وجود، فعال کرد:
using System; using System.Reflection; using System.Windows; using System.Windows.Controls; namespace DNTProfiler.Common.Behaviors { /// <summary> /// Smooth scrolling VirtualizingStackPanels, without sacrificing virtualization. /// </summary> public static class PixelBasedScrollingBehavior { private static readonly MethodInfo _setScrollUnit = typeof(VirtualizingPanel) .GetMethod("SetScrollUnit", BindingFlags.Public | BindingFlags.Static); private static readonly MethodInfo _setCacheLengthUnit = typeof(VirtualizingPanel) .GetMethod("SetCacheLengthUnit", BindingFlags.Public | BindingFlags.Static); private static readonly MethodInfo _setCacheLength = typeof(VirtualizingPanel) .GetMethod("SetCacheLength", BindingFlags.Public | BindingFlags.Static); public static bool GetIsEnabled(DependencyObject obj) { return (bool)obj.GetValue(IsEnabledProperty); } public static void SetIsEnabled(DependencyObject obj, bool value) { obj.SetValue(IsEnabledProperty, value); } public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(PixelBasedScrollingBehavior), new UIPropertyMetadata(false, handleIsEnabledChanged)); private static void handleIsEnabledChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { var listView = obj as ListView; if (listView == null) { throw new InvalidOperationException("This behavior can only be attached to a ListView."); } if (_setScrollUnit != null) { // It's .NET 4.5 _setScrollUnit.Invoke(listView, new object[] { listView, /*Pixel*/ 0 }); } if (_setCacheLengthUnit != null) { // It's .NET 4.5 _setCacheLengthUnit.Invoke(listView, new object[] { listView, /*Pixel*/ 0 }); } if (_setCacheLength != null) { // It's .NET 4.5 var type = Type.GetType("System.Windows.Controls.VirtualizationCacheLength, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"); if (type == null) return; var instance = Activator.CreateInstance(type, 100.0); _setCacheLength.Invoke(listView, new[] { listView, instance }); } } } }
برای استفاده از آن خواهیم داشت:
<ListView ItemsSource="{Binding}" behaviors:PixelBasedScrollingBehavior.IsEnabled="True">
- به صورت لیست
- به صورت لیست sort شده براساس Name
- فیلتر بر روی لیستی از فیلدهای موجود
- این لیست باید شامل تمامی دادههای موجود (چه در رم و چه در دیتابیس) باشد.
private static void GetLocalDestinationCount() { using (var context = new BreakAwayContext()) { var count = context.Destinations.Local.Count; Console.WriteLine("Destinations in memory: {0}", count); } }
استفاده از متد Load
private static void GetLocalDestinationCount() { using (var context = new BreakAwayContext()) { foreach (var destination in context.Destinations) { Console.WriteLine(destination.Name); } var count = context.Destinations.Local.Count; Console.WriteLine("Destinations in memory: {0}", count); } }
private static void GetLocalDestinationCountWithLoad() { using (var context = new BreakAwayContext()) { context.Destinations.Load(); var count = context.Destinations.Local.Count; Console.WriteLine("Destinations in memory: {0}", count); } }
private static void LoadAustralianDestinations() { using (var context = new BreakAwayContext()) { var query = from d in context.Destinations where d.Country == "Australia" select d; query.Load(); var count = context.Destinations.Local.Count; Console.WriteLine("Aussie destinations in memory: {0}", count); } }
private static void LocalLinqQueries() { using (var context = new BreakAwayContext()) { context.Destinations.Load(); var sortedDestinations = from d in context.Destinations.Local orderby d.Name select d; Console.WriteLine("All Destinations:"); foreach (var destination in sortedDestinations) { Console.WriteLine(destination.Name); } } var aussieDestinations = from d in context.Destinations.Local where d.Country == "Australia" select d; Console.WriteLine(); Console.WriteLine("Australian Destinations:"); foreach (var destination in aussieDestinations) { Console.WriteLine(destination.Name); } }
تفاوت بین Linq providerهای مختلف:
برش تصاویر قبل از آپلود (Crop)
<div style="overflow: auto"> <img id="preview" /> </div>
در این بین من با استفاده از فریمورک بوت استراپ کلاس img-responsive را به تگ image میدهم تا تصویر در هر صفحه نمایشی متناسب با اندازه آن نمایش داده شود. مشکل زیبایی تصویر در صفحه و نحوه کراپ کردن آن حل میشود ولی مشکل جدیدتر این است که تصویری که کراپ میشود آن ناحیه ای نیست که شما قبلا انتخاب کرده بودید؛ دلیل آن هم این است که تصویری که responsive شده است اندازه جدیدی برای خود دارد و برش ناحیه در سمت کلاینت و مختصاتی که به شما داده میشود بر اساس اندازه تغییر یافته است ولی در سمت سرور شما با اندازه واقعی تصویر سر و کار دارید و به همین دلیل مختصات ناحیه برش داده شده اشتباه بوده و قسمتهای دیگری از تصویر برش میخورند.
برای حل این مشکل ابتدا دو المان مخفی زیر را به صفحه اضافه میکنیم:
<input type="hidden" id="RealW" name="RealW" /> <input type="hidden" id="RealH" name="RealH" />
سپس کدهای زیر را به فایل js به شکل زیر ویرایش میکنیم:
$('#preview').Jcrop({ aspectRatio: 2, bgFade: true, bgOpacity: .3, onChange: updateInfo, onSelect: updateInfo }, function () { // use the Jcrop API to get the real image size //============== خطوط جدید $("#RealW").val($('#preview').css("width").replace("px", "")); $("#RealH").val($('#preview').css("height").replace("px", "")); //============== jcrop_api = this; });
سپس در سمت کلاینت با محاسبه فرمولهای تناسب این مشکل را رفع میکنیم تا مختصاتهای دریافت شده را به مختصاتهای واقعی تبدیل کنیم:
public static byte[] Resize(this byte[] byteImageIn, int x1,int y1,int x2,int y2,int realW,int realH) { //convert to full size image ImageConverter ic = new ImageConverter(); Image src = (Image)(ic.ConvertFrom(byteImageIn)); //original size if (src == null) return null; // چهار خط زیر فرمول تناسب را اجرا میکند x1 = src.Width * x1 / realW; x2 = src.Width * x2 / realW; y1 = src.Height * y1 / realH; y2 = src.Height * y2 / realH; Bitmap target = new Bitmap(x2 - x1, y2 - y1); using (Graphics graphics = Graphics.FromImage(target)) graphics.DrawImage(src, new Rectangle(0, 0, target.Width, target.Height), new Rectangle(x1,y1,x2-x1,y2-y1), GraphicsUnit.Pixel); src = target; using (var ms = new MemoryStream()) { src.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); return ms.ToArray(); } }
catch (Exception ex) { StringBuilder errorMsg = new StringBuilder(); for (Exception current = ex; current != null; current = current.InnerException) { if (errorMsg.Length > 0) errorMsg.Append("\n"); errorMsg.Append(current.Message.Replace("See the inner exception for details.", string.Empty)); } // log errorMsg.ToString(); }
public static string ExceptionToString(this Exception ex) { StringBuilder errorMsg = new StringBuilder(); for (Exception current = ex; current != null; current = current.InnerException) { if (errorMsg.Length > 0) errorMsg.Append("\n"); errorMsg.Append(current.Message. Replace("See the inner exception for details.", string.Empty)); } return errorMsg.ToString(); }
catch (Exception ex) { // log ex.ExceptionToString(); }
قابلیت جالبی از SQL Server 2005 به بعد به این محصول اضافه شده است که امکان ایجاد یک وب سرویس بومی را بر اساس رویههای ذخیره شده و یا توابع تعریف شده در دیتابیسهای موجود، فراهم میسازد. این قابلیت نیازی به IIS یا هر هاست دیگری برای اجرا ندارد و توسط خود اس کیوال سرور راه اندازی و مدیریت میشود.
توضیحات مفصل آنرا در MSDN میتوانید ملاحظه کنید و در اینجا یک مثال عملی از آن را با هم مرور خواهیم کرد:
الف) ایجاد یک جدول آزمایشی به همراه تعدادی رکورد دلخواه در آن
CREATE TABLE [tblWSTest](
[id] [int] IDENTITY(1,1) NOT NULL,
[f1] [nvarchar](50) NULL,
[f2] [nvarchar](500) NULL,
CONSTRAINT [PK_tblWSTest] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
SET IDENTITY_INSERT [tblWSTest] ON
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (1, N'a1', N'a2')
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (2, N'b1', N'b2')
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (3, N'c1', N'c2')
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (4, N'd1', N'd2')
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (5, N'e1', N'e2')
SET IDENTITY_INSERT [dbo].[tblWSTest] OFF
CREATE PROCEDURE GetAllData
AS
SELECT f1,
f2
FROM tblWSTest
CREATE ENDPOINT GetDataService
STATE = STARTED
AS HTTP(
PATH = '/GetData',
AUTHENTICATION = (INTEGRATED),
PORTS = (CLEAR),
CLEAR_PORT = 8080,
SITE = '*'
)
FOR SOAP(
WEBMETHOD 'GetAllData'
(NAME = 'testdb2009.dbo.GetAllData'),
WSDL = DEFAULT,
DATABASE = 'testdb2009',
NAMESPACE = DEFAULT
)
توضیحات:
Ports در حالت clear و یا ssl میتواند باشد. همچنین برای اینکه با IIS موجود بر روی سیستم هم تداخل نکند CLEAR_PORT به 8080 تنظیم شده است. سایر پارامترهای آن بسیار واضح هستند. برای مثال تعیین دیتابیسی که این رویه ذخیره شده در آن قرار دارد و همچنین مسیر کامل دسترسی به آن دقیقا مشخص میگردند.
این وب سرویس هم اکنون آغاز به کار کرده است. برای مشاهده wsdl آن، آدرس زیر را در مرورگر وب خود وارد نمائید (PATH و CLEAR_PORT معرفی شده در endPoint اینجا بکار میرود):
د) استفاده از این وب سرویس در یک برنامه ویندوزی
یک برنامه ساده winForms را شروع کنید. سپس یک DataGridView را بر روی فرم قرار دهید (بدیهی است این مورد میتواند یک برنامه ASP.Net هم باشد و موارد مشابه دیگر). سپس از منوی پروژه، یک service reference را در VS2008 بر اساس آدرس wdsl فوق اضافه کنید (شکل زیر):
برای اینکه این مثال در VS2008 درست کار کند باید فایل app.config ایجاد شده را کمی ویرایش کرد. قسمت security آن را یافته و تغییرات زیر را با توجه به AUTHENTICATION مورد نیاز تغییر دهید:
<security mode="TransportCredentialOnly">
<transport clientCredentialType="Windows" proxyCredentialType="None"
realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
using System;
using System.Data;
using System.Windows.Forms;
namespace WebServiceTest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
ServiceReference1.GetDataServiceSoapClient data =
new ServiceReference1.GetDataServiceSoapClient();
dataGridView1.DataSource = (data.GetAllData()[0] as DataSet).Tables[0];
}
}
}
»اولین راه حلی که به ذهن میرسد این است که پارامترهای مشخص شده را در متدهای سرویسهای مورد نظر قرار داد و به نوعی تمام سرویسها را به روز رسانی کرد. این روش به طور قطع در خیلی از قسمتهای پروژه به صورت مستقیم اثرگذار خواهد بود و در صورت نبود ابزارهای تست ممکن است با مشکلات جدی روبرو شوید.
»راه حل دوم این است که یک Message Header سفارشی بسازیم و در هر درخواست اطلاعات مورد نظر را در هدر قرار داده و سمت سرور این اطلاعات را به دست آوریم. این روش کمترین تغییر مورد نظر را برای پروژه دربر خواهد داشت و از طرفی نیاز متدهای سرویس به پارامتر را از بین میبرد و دیگر نیازی نیست تا تمام متدهای سرویسها دارای پارامترهای یکسان باشند.
پیاده سازی
برای شروع کلاس مورد نظر برای ارسال اطلاعات را به صورت زیر خواهیم ساخت:
[DataContract] public class ApplicationContext { [DataMember( IsRequired = true )] public string UserId { get { return _userId; } set { _userId = value; } } private string _userId; [DataMember( IsRequired = true )] public static ApplicationContext Current { get { return _current; } private set { _current = value; } } private static ApplicationContext _current;
public static void Register( ApplicationContext appContext ) { Current = appContext; IsRegistered = true; } }
public class ClientMessageHeaderInspector<T> : IClientMessageInspector { private readonly T _vaccine; public ClientMessageHeaderInspector( T vaccine ) { this._vaccine = vaccine; } public void AfterReceiveReply( ref Message reply, object correlationState ) { } public object BeforeSendRequest( ref Message request, IClientChannel channel ) { MessageHeader messageHeader = MessageHeader.CreateHeader( typeof( T ).Name, typeof( T ).Namespace, this._vaccine ); request.Headers.Add( messageHeader ); return null; } }
public class ApplicationContextMessageBehavior : IEndpointBehavior { ClientMessageHeaderInspector<ApplicationContext> inspector = null; public ApplicationContextMessageBehavior() { inspector = new ClientMessageHeaderInspector<ApplicationContext>( ApplicationContext.Current ); } public void AddBindingParameters( ServiceEndpoint endpoint, BindingParameterCollection bindingParameters ) { } public void ApplyClientBehavior( ServiceEndpoint endpoint, ClientRuntime clientRuntime ) { clientRuntime.MessageInspectors.Add( inspector ); } public void ApplyDispatchBehavior( ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher ) { } public void Validate( ServiceEndpoint endpoint ) { } }
در مرحله آخر باید تنظیمات مربوط به ChannelFactory را انجام دهیم.
public class ServiceMapper<TChannel> { internal static EndpointAddress EPAddress { get { return _epAddress; } } private static EndpointAddress _epAddress; public static TChannel CreateChannel( Binding binding, string uriBase, string serviceName, bool setCredential ) { _epAddress = new EndpointAddress( String.Format( "{0}{1}", uriBase, serviceName ) ); var factory = new ChannelFactory<TChannel>( binding, _epAddress ); ApplicationContext.Register( new ApplicationContext { UserId = Guid.NewGuid() } );
factory.Endpoint.Behaviors.Add( new ApplicationContextMessageBehavior() ); TChannel proxy = factory.CreateChannel(); if ( factory.Endpoint.Behaviors.OfType<ApplicationContextMessageBehavior>().Any() ) { using ( var scope = new OperationContextScope( ( IClientChannel )proxy ) ) { OperationContext.Current.OutgoingMessageHeaders.Add( MessageHeader.CreateHeader( typeof( ApplicationContext ).Name, typeof( ApplicationContext ).Namespace, ApplicationContext.Current ) ); } } return proxy; }
»در متد CreateChannel، ابتدا تنظیمات مربوط به EndPointAddress و ChannelFactory انجام میشود. سپس یک نمونه از کلاس ApplicationContext را توسط متد Register به کلاس مورد نظر رجیستر میکنیم. به این ترتیب مقدار خاصیت Current در کلاس ApplicationContext برابر با نمونه ساخته شده میشود. سپس کلاس ApplicationContextMessageBehavior به خاصیت Behavior در ChannelFactory اضافه میشود. در انتها نیز هدر سفارشی ساخته شده به MessageHeaderهای نمونه جاری OperationContext اضافه میشود. این عمل توسط کد زیر انجام میگیرد:
OperationContext.Current.OutgoingMessageHeaders.Add( MessageHeader.CreateHeader( typeof( ApplicationContext ).Name, typeof( ApplicationContext ).Namespace, AppConfiguration.Application ) );
استفاده از هدر سفارشی سمت سرور
حال قصد داریم که اطلاعات مورد نظر را از هدر درخواست در سمت سرور به دست آورده و از آن در کوئریهای خود استفاده نماییم. کد زیر این کار را برای ما انجام میدهد:
if ( OperationContext.Current != null && OperationContext.Current.IncomingMessageHeaders.FindHeader( typeof( ApplicationContext ).Name , typeof( ApplicationContext ).Namespace ) > 0 ) { _application = OperationContext.Current.IncomingMessageHeaders.GetHeader<ApplicationContext>( typeof( ApplicationContext ).Name , typeof( ApplicationContext ).Namespace ); }
فارسی نویسی و iTextSharp
var m = new ITModel.ITModelContainer(); var list = (from pp in m.PERSONNELs select pp).ToList(); string pdfpath = Server.MapPath("PDFs"); using (var pdfDoc = new Document(PageSize.A4)) { var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream(pdfpath + "/Personnel2.pdf", FileMode.Create)); pdfDoc.Open(); var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf"; var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); var tahomaFont = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK); float[] widths = new float[] { 1f, 2f }; PdfPTable table = new PdfPTable(2) { TotalWidth = 216f, LockedWidth = true, HorizontalAlignment = 0, SpacingBefore = 20f, SpacingAfter = 30f }; table.SetWidths(widths); PdfPCell cell = new PdfPCell(new Phrase("لیست پرسنل", tahomaFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, Colspan = 2, Border = 0, HorizontalAlignment = 1 }; table.AddCell(cell); foreach (var item in list) { PdfPCell cell2 = new PdfPCell(new Phrase(item.PERSON_ID.ToString(), tahomaFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL }; PdfPCell cell3 = new PdfPCell(new Phrase(item.FIRST_NAME + " " + item.LAST_NAME, tahomaFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL }; table.AddCell(cell2); table.AddCell(cell3); } pdfDoc.Add(table); }
{ RunDirection = PdfWriter.RUN_DIRECTION_RTL };
ممنون میشم راهنمایی کنید
مرسی
روشهای زیادی برای پیاده سازی این نوع جستجوها وجود دارد. در این مقاله سعی شده گامهای ایجاد یک ساختار پایه برای این نوع فرمها و یک ایجاد فرم نمونه بر پایه ساختار ایجاد شده را با استفاده از یکی از همین روشها شرح دهیم.
اساس این روش تولید عبارت Linq بصورت پویا با توجه به انتخابهای کاربرمی باشد.
1- برای شروع یک سلوشن خالی با نام DynamicSearch ایجاد میکنیم. سپس ساختار این سلوشن را بصورت زیر شکل میدهیم.
در این مثال پیاده سازی در قالب ساختار MVVM در نظر گرفته شده. ولی محدودتی از این نظر برای این روش قائل نیستیم.
2- کار را از پروژه مدل آغاز میکنیم. جایی که ما برای سادگی کار، 3 کلاس بسیار ساده را به ترتیب زیر ایجاد میکنیم:
namespace DynamicSearch.Model { public class Person { public Person(string name, string family, string fatherName) { Name = name; Family = family; FatherName = fatherName; } public string Name { get; set; } public string Family { get; set; } public string FatherName { get; set; } } } using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DynamicSearch.Model { public class Teacher : Person { public Teacher(int id, string name, string family, string fatherName) : base(name, family, fatherName) { ID = id; } public int ID { get; set; } public override string ToString() { return string.Format("Name: {0}, Family: {1}", Name, Family); } } } namespace DynamicSearch.Model { public class Student : Person { public Student(int stdId, Teacher teacher, string name, string family, string fatherName) : base(name, family, fatherName) { StdID = stdId; Teacher = teacher; } public int StdID { get; set; } public Teacher Teacher { get; set; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using DynamicSearch.Model; namespace DynamicSearch.Service { public class StudentService { public IList<Student> GetStudents() { return new List<Student> { new Student(1,new Teacher(1,"Ali","Rajabi","Reza"),"Mohammad","Hoeyni","Sadegh"), new Student(2,new Teacher(2,"Hasan","Noori","Mohsen"),"Omid","Razavi","Ahmad"), }; } } }
جهت ساخت عبارت، نیاز به سه نوع جزء داریم:
-اتصال دهنده عبارات ( "و" ، "یا")
-عملوند (در اینجا فیلدی که قصد مقایسه با عبارت مورد جستجوی کاربر را داریم)
-عملگر ("<" ، ">" ، "=" ، ....)
برای ذخیره المانهای انتخاب شده توسط کاربر، سه کلاس زیر را ایجاد میکنیم (همان سه جزء بالا):
using System; using System.Linq.Expressions; namespace DynamicSearch.ViewModel.Base { public class AndOr { public AndOr(string name, string title,Func<Expression,Expression,Expression> func) { Title = title; Func = func; Name = name; } public string Title { get; set; } public Func<Expression, Expression, Expression> Func { get; set; } public string Name { get; set; } } } using System; namespace DynamicSearch.ViewModel.Base { public class Feild : IEquatable<Feild> { public Feild(string title, Type type, string name) { Title = title; Type = type; Name = name; } public Type Type { get; set; } public string Name { get; set; } public string Title { get; set; } public bool Equals(Feild other) { return other.Title == Title; } } } using System; using System.Linq.Expressions; namespace DynamicSearch.ViewModel.Base { public class Operator { public enum TypesToApply { String, Numeric, Both } public Operator(string title, Func<Expression, Expression, Expression> func, TypesToApply typeToApply) { Title = title; Func = func; TypeToApply = typeToApply; } public string Title { get; set; } public Func<Expression, Expression, Expression> Func { get; set; } public TypesToApply TypeToApply { get; set; } } }
using System.Collections.ObjectModel; using System.Linq; using System.Linq.Expressions; namespace DynamicSearch.ViewModel.Base { public abstract class SearchFilterBase<T> : BaseViewModel { protected SearchFilterBase() { var containOp = new Operator("شامل باشد", (expression, expression1) => Expression.Call(expression, typeof(string).GetMethod("Contains"), expression1), Operator.TypesToApply.String); var notContainOp = new Operator("شامل نباشد", (expression, expression1) => { var contain = Expression.Call(expression, typeof(string).GetMethod("Contains"), expression1); return Expression.Not(contain); }, Operator.TypesToApply.String); var equalOp = new Operator("=", Expression.Equal, Operator.TypesToApply.Both); var notEqualOp = new Operator("<>", Expression.NotEqual, Operator.TypesToApply.Both); var lessThanOp = new Operator("<", Expression.LessThan, Operator.TypesToApply.Numeric); var greaterThanOp = new Operator(">", Expression.GreaterThan, Operator.TypesToApply.Numeric); var lessThanOrEqual = new Operator("<=", Expression.LessThanOrEqual, Operator.TypesToApply.Numeric); var greaterThanOrEqual = new Operator(">=", Expression.GreaterThanOrEqual, Operator.TypesToApply.Numeric); Operators = new ObservableCollection<Operator> { equalOp, notEqualOp, containOp, notContainOp, lessThanOp, greaterThanOp, lessThanOrEqual, greaterThanOrEqual, }; SelectedAndOr = AndOrs.FirstOrDefault(a => a.Name == "Suppress"); SelectedFeild = Feilds.FirstOrDefault(); SelectedOperator = Operators.FirstOrDefault(a => a.Title == "="); } public abstract IQueryable<T> GetQuarable(); public virtual ObservableCollection<AndOr> AndOrs { get { return new ObservableCollection<AndOr> { new AndOr("And","و", Expression.AndAlso), new AndOr("Or","یا",Expression.OrElse), new AndOr("Suppress","نادیده",(expression, expression1) => expression), }; } } public virtual ObservableCollection<Operator> Operators { get { return _operators; } set { _operators = value; NotifyPropertyChanged("Operators"); } } public abstract ObservableCollection<Feild> Feilds { get; } public bool IsOtherFilters { get { return _isOtherFilters; } set { _isOtherFilters = value; } } public string SearchValue { get { return _searchValue; } set { _searchValue = value; NotifyPropertyChanged("SearchValue"); } } public AndOr SelectedAndOr { get { return _selectedAndOr; } set { _selectedAndOr = value; NotifyPropertyChanged("SelectedAndOr"); NotifyPropertyChanged("SelectedFeildHasSetted"); } } public Operator SelectedOperator { get { return _selectedOperator; } set { _selectedOperator = value; NotifyPropertyChanged("SelectedOperator"); } } public Feild SelectedFeild { get { return _selectedFeild; } set { Operators = value.Type == typeof(string) ? new ObservableCollection<Operator>(Operators.Where(a => a.TypeToApply == Operator.TypesToApply.Both || a.TypeToApply == Operator.TypesToApply.String)) : new ObservableCollection<Operator>(Operators.Where(a => a.TypeToApply == Operator.TypesToApply.Both || a.TypeToApply == Operator.TypesToApply.Numeric)); if (SelectedOperator == null) { SelectedOperator = Operators.FirstOrDefault(a => a.Title == "="); } NotifyPropertyChanged("SelectedOperator"); NotifyPropertyChanged("SelectedFeild"); _selectedFeild = value; NotifyPropertyChanged("SelectedFeildHasSetted"); } } public bool SelectedFeildHasSetted { get { return SelectedFeild != null && (SelectedAndOr.Name != "Suppress" || !IsOtherFilters); } } private ObservableCollection<Operator> _operators; private Feild _selectedFeild; private Operator _selectedOperator; private AndOr _selectedAndOr; private string _searchValue; private bool _isOtherFilters = true; } }
در گام بعد، یک کلاس کمکی برای سهولت ساخت عبارات ایجاد میکنیم:
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using AutoMapper; namespace DynamicSearch.ViewModel.Base { public static class ExpressionExtensions { public static List<T> CreateQuery<T>(Expression whereCallExpression, IQueryable entities) { return entities.Provider.CreateQuery<T>(whereCallExpression).ToList(); } public static MethodCallExpression CreateWhereCall<T>(Expression condition, ParameterExpression pe, IQueryable entities) { var whereCallExpression = Expression.Call( typeof(Queryable), "Where", new[] { entities.ElementType }, entities.Expression, Expression.Lambda<Func<T, bool>>(condition, new[] { pe })); return whereCallExpression; } public static void CreateLeftAndRightExpression<T>(string propertyName, Type type, string searchValue, ParameterExpression pe, out Expression left, out Expression right) { var typeOfNullable = type; typeOfNullable = typeOfNullable.IsNullableType() ? typeOfNullable.GetTypeOfNullable() : typeOfNullable; left = null; var typeMethodInfos = typeOfNullable.GetMethods(); var parseMethodInfo = typeMethodInfos.FirstOrDefault(a => a.Name == "Parse" && a.GetParameters().Count() == 1); var propertyInfos = typeof(T).GetProperties(); if (propertyName.Contains(".")) { left = CreateComplexTypeExpression(propertyName, propertyInfos, pe); } else { var propertyInfo = propertyInfos.FirstOrDefault(a => a.Name == propertyName); if (propertyInfo != null) left = Expression.Property(pe, propertyInfo); } if (left != null) left = Expression.Convert(left, typeOfNullable); if (parseMethodInfo != null) { var invoke = parseMethodInfo.Invoke(searchValue, new object[] { searchValue }); right = Expression.Constant(invoke, typeOfNullable); } else { //type is string right = Expression.Constant(searchValue.ToLower()); var methods = typeof(string).GetMethods(); var firstOrDefault = methods.FirstOrDefault(a => a.Name == "ToLower" && !a.GetParameters().Any()); if (firstOrDefault != null) left = Expression.Call(left, firstOrDefault); } } public static Expression CreateComplexTypeExpression(string searchFilter, IEnumerable<PropertyInfo> propertyInfos, Expression pe) { Expression ex = null; var infos = searchFilter.Split('.'); var enumerable = propertyInfos.ToList(); for (var index = 0; index < infos.Length - 1; index++) { var propertyInfo = infos[index]; var nextPropertyInfo = infos[index + 1]; if (propertyInfos == null) continue; var propertyInfo2 = enumerable.FirstOrDefault(a => a.Name == propertyInfo); if (propertyInfo2 == null) continue; var val = Expression.Property(pe, propertyInfo2); var propertyInfos3 = propertyInfo2.PropertyType.GetProperties(); var propertyInfo3 = propertyInfos3.FirstOrDefault(a => a.Name == nextPropertyInfo); if (propertyInfo3 != null) ex = Expression.Property(val, propertyInfo3); } return ex; } public static Expression AddOperatorExpression(Func<Expression, Expression, Expression> func, Expression left, Expression right) { return func.Invoke(left, right); } public static Expression JoinExpressions(bool isFirst, Func<Expression, Expression, Expression> func, Expression expression, Expression ex) { if (!isFirst) { return func.Invoke(expression, ex); } expression = ex; return expression; } } }
using System.Collections.ObjectModel; using System.Linq; using DynamicSearch.Model; using DynamicSearch.Service; using DynamicSearch.ViewModel.Base; namespace DynamicSearch.ViewModel { public class StudentSearchFilter : SearchFilterBase<Student> { public override ObservableCollection<Feild> Feilds { get { return new ObservableCollection<Feild> { new Feild("نام دانش آموز",typeof(string),"Name"), new Feild("نام خانوادگی دانش آموز",typeof(string),"Family"), new Feild("نام خانوادگی معلم",typeof(string),"Teacher.Name"), new Feild("شماره دانش آموزی",typeof(int),"StdID"), }; } } public override IQueryable<Student> GetQuarable() { return new StudentService().GetStudents().AsQueryable(); } } }
در نهایت زمل فایل موجود در پروژه ویو:
<Window x:Class="DynamicSearch.View.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:viewModel="clr-namespace:DynamicSearch.ViewModel;assembly=DynamicSearch.ViewModel" xmlns:view="clr-namespace:DynamicSearch.View" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> <Window.Resources> <viewModel:StudentSearchViewModel x:Key="StudentSearchViewModel" /> <view:VisibilityConverter x:Key="VisibilityConverter" /> </Window.Resources> <Grid DataContext="{StaticResource StudentSearchViewModel}"> <WrapPanel Orientation="Vertical"> <DataGrid AutoGenerateColumns="False" Name="asd" CanUserAddRows="False" ItemsSource="{Binding BindFilter}"> <DataGrid.Columns> <DataGridTemplateColumn> <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <ComboBox MinWidth="100" DisplayMemberPath="Title" ItemsSource="{Binding AndOrs}" Visibility="{Binding IsOtherFilters,Converter={StaticResource VisibilityConverter}}" SelectedItem="{Binding SelectedAndOr,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <DataGridTemplateColumn > <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <ComboBox IsEnabled="{Binding SelectedFeildHasSetted}" MinWidth="100" DisplayMemberPath="Title" ItemsSource="{Binding Feilds}" SelectedItem="{Binding SelectedFeild,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged }"/> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <DataGridTemplateColumn> <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <ComboBox MinWidth="100" DisplayMemberPath="Title" ItemsSource="{Binding Operators}" IsEnabled="{Binding SelectedFeildHasSetted}" SelectedItem="{Binding SelectedOperator,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <DataGridTemplateColumn Width="*"> <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <TextBox IsEnabled="{Binding SelectedFeildHasSetted}" MinWidth="200" Text="{Binding SearchValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/> <!--<TextBox Text="{Binding SearchValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>--> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid> <Button Content="+" HorizontalAlignment="Left" Command="{Binding AddFilter}"/> <Button Content="Result" Command="{Binding ExecuteSearchFilter}"/> <DataGrid ItemsSource="{Binding Results}"> </DataGrid> </WrapPanel> </Grid> </Window>
برخی منابع جهت آشنایی با Expression ها:
http://msdn.microsoft.com/en-us/library/bb882637.aspx
انتخاب پویای فیلدها در LINQ
http://www.persiadevelopers.com/articles/dynamiclinqquery.aspx
نکته: کدهای نوشته شده در این مقاله، نسخههای نخستین هستند و طبیعتا جا برای بهبود بسیار دارند. دوستان میتوانند در این امر به بنده کمک کنند.
پیشنهادات جهت بهبود:
- جداسازی کدهای پیاده کننده منطق از ویو مدلها جهت افزایش قابلیت نگهداری کد و سهولت استفاده در سایر ساختارها
- افزودن توضیحات به کد
- انتخاب نامگذاریهای مناسب تر
DynamicSearch.zip
اجرای این نوع صفحات کار سختی نیست؛ با کمی جستجو در اینترنت مثلا در اینجا میتوانید چیزهای خوبی پیدا کنید. اما متاسفانه اکثر مثالها چیزی شبیه قرار دادن پارشال "ورود اعضا" در کنار پارشال "ثبت نام" هستند. حتما متوجه شدهاید که معمولا این دو صفحه پس از PostBack به صفحهای جدید هدایت میشوند و یا در بهترین حالت به کمک Ajax ، پس از انجام عملیات، پیامی به کاربر نمایش میدهیم.
در این مقاله سعی شده روشی برای ایجاد چند فرم در یک View توضیح داده شود با این شرط که:
اولا : از Ajax یا هلپر ایجکسی استفاده نکنیم.
ثانیا : پس از post-back، عملیات Redirect را انجام ندهیم و صفحه جاری را حفظ کنیم؛ چه قرار باشد همه چیز درست انجام شده باشد و چه مشکلی پیش آمده باشد و پیام خطایی در کنار فیلدها نمایش داده شود.
در این روش به این نکته توجه شده که هر مدل پس از Post-back حفظ شود و مستقل از دیگری رفتار کند. مثلا
اگر یکی از فرمها ناقص پر شد و دکمهی ارسال آن فشرده شد، پس از Post-back، فقط و فقط اجزای همین فرم Validate شود و فرم دوم بدون تغییر باقی بماند.
ویوی زیر را در نظر بگیرید. در layout، دو پارشال، به
کمک اکشنمتد فراخوانی شدهاند:
ViewModelهای مرتبط با این دو بخش به شکل زیر هستند :
ContactVM .cs
public class ContactVM { [Display(Name = "نام")] [Required(ErrorMessage = "لطفا {0} را وارد کنید")] public string Name { get; set; } [EmailAddress(ErrorMessage = "آدرس ایمیل صحیح نیست")] [DataType(DataType.EmailAddress)] [Display(Name = "آدرس ایمیل")] [Required(ErrorMessage = "لطفا {0} را وارد کنید")] public string EmailAddress { get; set; } [Display(Name = "متن پیام")] [Required(ErrorMessage = "حرفی برای گفتن ندارید؟")] public string Description { get; set; } [Required(ErrorMessage = "لطفا {0} را وارد کنید")] [Display(Name = "حاصل جمع")] public string Captcha { get; set; } }
SubscriberVM .cs
public class SubscriberVM { /*[RegularExpression("^[a-zA-Z0-9_\\.-]+@([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$", ErrorMessage = "آدرس ایمیل صحیح نیست")]*/ [EmailAddress(ErrorMessage = "آدرس ایمیل صحیح نیست")] /*.Net4.5*/ [Display(Name = "ایمیل")] [Required(ErrorMessage = "لطفا {0} را وارد کنید")] public string Email { get; set; } [Display(Name = "وضعیت")] public bool IsActive { get; set; } }
در Layout، دو اکشن متد صدا زده شدهاند که وظیفه ارسال ویوهای هر کدام به Layout را به عهده دارند :
<div class="row footerclass"> <div class="col-md--6"> @Html.Action("Subscribers", "Home") </div> <div class="col-md-6"> @Html.Action("Contact", "Home") </div> </div>
اکشن متدهای این دو پارشال به شکل زیر هستند :
public ActionResult Contact() { return PartialView("_Contact", model); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Contact(ContactVM model) { if (ModelState.IsValid) { //Do Something } return PartialView("_Contact", model); } public ActionResult Subscribers() { return PartialView("_Subscribers"); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Subscribers(SubscriberVM model) { if (ModelState.IsValid) { //Do Something } } return PartialView("_Subscribers",model); }
و اما ویوهایی که قرار است نمایش داده شوند:
Contact.Cshtml _
@model IrsaShop.Models.ViewModel.ContactVM <span></span><span>تماس با ما</span> <hr /> @using (Html.BeginForm()) { @Html.AntiForgeryToken() @Html.ValidationSummary(true) <div> @Html.TextBoxFor(m => m.Name, new { @class = "form-control", @id = "name", @name = "name", placeholder = "نام" }) @Html.ValidationMessageFor(m => m.Name) </div> <div> @Html.TextBoxFor(m => m.EmailAddress, new { @class = "form-control", @id = "email", @name = "email", placeholder = "ایمیل", @style = "direction: ltr" }) @Html.ValidationMessageFor(m => m.EmailAddress) </div> <div> @Html.TextAreaFor(model => model.Description, new { @class = "form-control", @id = "message", @name = "message", placeholder = "پیام", @style = "max-width: 100%;height: 90px;" }) </div> <div> <input type="button" value="" id="refresh" /> <img alt="Captcha" id="imgcpatcha" src="@Url.Action("CaptchaImage","Captcha")" /> </div> <div> @Html.TextBoxFor(model => model.Captcha, new { @class = "form-control", placeholder = "حاصل جمع؟" }) @Html.ValidationMessageFor(model => model.Captcha) </div> <div> <input type="submit" value="ارسال" name="submitValue" /> </div> }
_Subscriber.Csh tml
@model IrsaShop.Models.SubscriberVM <span></span><span>خبرنامه</span> <hr/> @using (Html.BeginForm()) { @Html.AntiForgeryToken() @Html.ValidationSummary(true) <div> <div> @Html.TextBoxFor(m => m.Email, new { @class = "form-control right-buffer top-buffer pull-right", @id = "email", @name = "email", placeholder = "ایمیل", @style = "direction: ltr;width: 50%", @required = "required" }) @* <button type="submit" name="submitValue">ثبت ایمیل</button>*@ <input type="submit" value="ثبت ایمیل" name="submitValue" /> </div> </div> @Html.ValidationMessageFor(m => m.Email,"",new { @class = "right-buffer pull-right"}) }
نکته اول : هیچ نوع ورودی برای Html.BeginForm در نظر گرفته نشده است.
اگر اکشن متدی را برای صدا زدن در این بخش در نظر بگیرید، هنگام Postback به مشکل
برخورد خواهید کرد؛ چون آدرس آن اکشن متد به شکل صریح در آدرس مرورگر فراخوانی میشود و
پارشال ما پس از Post-back به تنهایی
و بدون Layout نمایش داده خواهد شد. اسم بردن از اکشن متد وقتی کارساز است که آن اکشن متد
قرار باشد یک Redirect انجام دهد ولی هدف ما این
است که صفحه را از دست ندهیم و پیامهای خطای ModelState را در همان صفحه قبل و پس
از Post-back ببینیم و همچنین پس از انجام عملیات (مثلا
ارسال پیام) همین صفحه نمایش داده شود.
نکته دوم : نکته اول یک مشکل دارد! اگر به شکل صریح اکشن متد مربوط به Post-back مشخص نشود، بطور اتوماتیک تمامی اکشن متدهایی که ویژگی [HttpPost] دارند اجرا خواهند شد. این یعنی هر دو اکشن متد Contact و Subscriber اجرا میشوند و بنابر آنچه در اکشن متدها نوشتهایم هر دو ModelState بررسی میشود که این هدف ما نیست. مثلا فرم سمت چپ را تکمیل کرده ایم و دکمه "ثبت ایمیل" را فشار دادهایم و صفحه Postback میشود و با اینکه ایمیل در بانک ثبت شده اما فرم سمت راستی با خطا ظاهر میشود که چرا فیلدها خالی هستند!؟
برای حل این مشکل کافیست خاصیت name مربوط به دکمهها را به شکل یک ورودی برای اکشن متدها بفرستیم و بر اساس وضعیت آن تنها state مدل مورد نظر خودمان را بررسی کنیم. پس اصلاح زیر را برای اکشن متدهای دارای ویژگی [HttpPost] انجام میدهیم.
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Contact(ContactVM model, , string submitValue) { if (submitValue == "ارسال") { if (ModelState.IsValid) { //Do Something } } else { ModelState.Clear(); } return PartialView("_Subscribers", model); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Subscribers(SubscriberVM model, string submitValue) { if (submitValue == "ثبت ایمیل") { if (ModelState.IsValid) { //Do Something } } else { ModelState.Clear(); } return PartialView("_Subscribers"); }
نکته سوم : در این روش سعی کنید از ViewModel استفاده کنید و سعی کنید ویو مدلها پراپرتیهای با نام یکسان نداشته باشند. مثلا پراپرتی Email در ویو مدلها نامهای متفاوتی داشته باشند (مثل EmailAddress ، Email ، ContactMail و ...). با اینکار در زمان Postback احتمال اینکه فیلدهای مشترک اتوماتیک پر شده به ما نمایش داده باشند صفر خواهد شد.
نکته چهارم : حواستان باشد پس از انجام عملیات مرتبط با هر فرم در اکشن متد مربوط به آن (مثلا ارسال ایمیل، ثبت در بانک یا ...) در صورتی که عملیات با موفقیت انجام شد حتما ModelState را clear کنید. با اینکار پس از Post-back فیلدهای پارشالها خالی میشوند.
نکته پنجم : میتوانید به سادگی مدیریت خطا را به کمک جی کوئری انجام دهید؛ مثلا فرض کنید میخواهیم اگر ایمیل کاربر برای دریافت خبرنامه با موفقیت ثبت شد، پیامی مبنی بر موفقیت برای وی بفرستیم؛ اکشن متد HttPost مربوط به Subscriber را به شکل زیر تکمیل میکنیم :
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Subscribers(SubscriberVM model, string submitValue) { if (submitValue == "ثبت ایمیل") { if (ModelState.IsValid) { Subscriber mail = new Subscriber() { Email = model.EmailSubscriber, IsActive = true }; context.Subscribers.Add(mail); context.SaveChanges(); ViewBag.info = "ایمیل شما با موفقیت ثبت شد."; ViewBag.color = "alert-success"; ModelState.Clear(); } } else { ModelState.Clear(); } return PartialView("_Subscribers "); }
در انتهای پارشال _Subscriber هم چند خط کد زیر را مینویسیم :
@if (!String.IsNullOrEmpty(ViewBag.info)) { <div id="info" style="position: fixed; bottom: 0; right: 0; margin-right: 1%;"> <div class="alert @ViewBag.color alert-dismissable"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <strong> @ViewBag.info</strong> </div> </div> <script> $(function () { $("#info").fadeOut(15000); }); </script> }
نتیجه این خواهد بود که پس از PostBack در صورت موفقیت تصویر زیر را خواهیم دید و 15 ثانیه المان سبزرنگ بوت استرپِ زیر نمایش داده خواهد شد.
این روش نوعی مدیریت میان اکشن متدهای دارای ویژگی HttpPost است و همانطور که گفتیم به علت اینکه پس از Post-Back نیاز به ساختار به هم نخوردهی صفحهی قبلی داریم، نمیتوانستیم به شکل صریح، اکشن متد برای Html.BeginForm تعریف کنیم تا این دردسرها را نداشته باشیم.
حین نوشتن این مقاله به علت وجود if های تو در تو، امیدوار بودم که روشهای بهتری برای اینکار موجود باشند و هنوز هم امیدوارم نظرات شما چنین چیزی را نشان دهد.