PDA

View Full Version : مشکل اعتبارسنجی Binding در DataGrid



SajjadKhati
جمعه 05 اسفند 1401, 11:19 صبح
سلام دوستان .
یه DataGrid دارم که به یه ObservableCollection<Person> ، بایندینگ (Binding) شد .
کلاس Person ام پروپرتی هایی از قبیل PersonID و FirstName و LastName و PhoneNumber و NationalCode داره .

میخوام اعتبارسنجیِ این بایندینگ را بعد از اینکه کاربر ، مقداری را در DataGrid وارد کرد و قبل از اینکه به Binding Source بره ، این اعتبار سنجی انجام بشه و تصمیم گرفته بشه که بره یا نره .

طبق آموزش در لینک زیر :

How to: Implement Validation with the DataGrid Control - WPF .NET Framework | Microsoft Learn (https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/how-to-implement-validation-with-the-datagrid-control?view=netframeworkdesktop-4.8#to-validate-multiple-values-in-a-single-row)

--------

من این کارها را انجام دادم اما مشکل اینه که وقتی اطلاعات یه سطر را وارد کردم و دکمه ی Enter در کیبرد را زدم (یعنی اطلاعات سطر وارد شد) ، در متد Validate در کلاس DataGridItemsSource_Persons_ValidationRule (در کد زیر) ، مقدار متغییر person در این متد را وقتی میگیرم ، مقدارش ، همون اطلاعاتی نیست که کاربر وارد کرده بود .

بلکه مقدار متغییر person ،اطلاعاتی هست که (تا اون لحظه که در متد Validate هست) ، درون همون Binding Source Object بود . یعنی تا زمانی که اون متد اجرا بشه ، اطلاعات آیتم مورد نظر در پروپرتیِ Persons (در کلاس PhoneBook) را بهم نشون میده که این اطلاعات از قبل بود و برام مهم نیست .

بلکه من در متد Validate ، اطلاعاتی که کاربر ویرایش کرد را نیاز دارم تا تصمیم بگیرم که آیا مقدار جدیدی که وارد شد ، معتبر هست یا نه .

کد xaml در MainWindow.xaml :



<Window x:Class="WPF_Practice_2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_Practice_2"
xmlns:localClasses="clr-namespace:WPF_Practice_2.Classes"
xmlns:bindingValidations="clr-namespace:WPF_Practice_2.Classes.BindingValidation s"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">


<Window.Resources>


<localClasses:ViewModel x:Key="ViewModelKey"/>


<CollectionViewSource x:Key="PersonsCollectionViewSourceKey"
Source="{Binding
Source={StaticResource ViewModelKey},
Path=PhoneBook.Persons}"/>


<ControlTemplate TargetType="{x:Type Control}" x:Key="PersonValidationRulesControlTemplateKey">
<Grid Margin="0,-2,0,-2"
ToolTip="{Binding RelativeSource={RelativeSource
FindAncestor, AncestorType={x:Type DataGridRow}},
Path=(Validation.Errors)[0].ErrorContent}">
<Ellipse StrokeThickness="0" Fill="Red"
Width="{TemplateBinding FontSize}"
Height="{TemplateBinding FontSize}" />
<TextBlock Text="!" FontSize="{TemplateBinding FontSize}"
FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" />
</Grid>
</ControlTemplate>


</Window.Resources>




<Grid DataContext="{Binding
Source={StaticResource PersonsCollectionViewSourceKey}}">


<DataGrid Name="PhoneBookDataGrid" HorizontalAlignment="Center" Height="271"
Margin="0,153,0,0" VerticalAlignment="Top" Width="780"
RowValidationErrorTemplate="{StaticResource PersonValidationRulesControlTemplateKey}"
ItemsSource="{Binding}"


<DataGrid.RowValidationRules>
<bindingValidations:DataGridItemsSource_Persons_Val idationRule/>
</DataGrid.RowValidationRules>

</DataGrid>

</Grid>
</Window>




کد کلاس اعتبار سنجی :



using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;


namespace WPF_Practice_2.Classes.BindingValidations
{
public class DataGridItemsSource_Persons_ValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
BindingGroup bindingGroup = value as BindingGroup;
if (bindingGroup == null)
return new ValidationResult(false, "عمل تبدیل ، ناموفق بود .");
Person person = bindingGroup.Items[0] as Person;
if (person == null)
return new ValidationResult(false, "عمل تبدیل ، ناموفق بود .");




if (person.PersonID < 0)
return new ValidationResult(false, "شناسه باید عددی مثبت باشد");


return ValidationResult.ValidResult;
}






}
}





بقیه ی کدهای کلاس های سی شارپ :



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Navigation;


namespace WPF_Practice_2.Classes
{
public class Person ////: IDataErrorInfo
{
public int PersonID { get; set; }


public string FirstName { get; set; }


public string LastName { get; set; }


public long PhoneNumber { get; set; }


public string NationalCode { get; set; }








public Person()
{


}




public Person(string firstName, string lastName, long phoneNumber, string nationalCode)
{
this.FirstName = firstName;
this.LastName = lastName;
this.PhoneNumber = phoneNumber;
this.NationalCode = nationalCode;
}




public Person(int personID, string firstName, string lastName, long phoneNumber, string nationalCode)
{
this.PersonID = personID;
this.FirstName = firstName;
this.LastName = lastName;
this.PhoneNumber = phoneNumber;
this.NationalCode = nationalCode;
}


}
}




و



using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;


namespace WPF_Practice_2.Classes
{
public class PhoneBook
{


public ObservableCollection<Person> Persons { get; set; }


public PhoneBook(ObservableCollection<Person> persons)
{
this.Persons = persons;
}






}
}


و



using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;


namespace WPF_Practice_2.Classes
{
public class ViewModel
{
public PhoneBook PhoneBook { get; set; }


public ViewModel()
{
ObservableCollection<Person> persons = new ObservableCollection<Person>();
Person person = new Person(574, "احمد", "متوسلیان", 109854, "574");
Person person2 = new Person(65, "غلامحسین", "افشردی", 58, "12");
Person person3 = new Person(84, "محمد", "جهان آرا", 09115478, "00");
Person person4 = new Person(313, "قاسم", "سلیمانی", 98, "");
Person person5 = new Person(879, "سید مجتبی", "میر لوحی", 42515874, "7744");


persons.Add(person);
persons.Add(person2);
persons.Add(person3);
persons.Add(person4);
persons.Add(person5);


this.PhoneBook = new PhoneBook(persons);
}




}
}



==============

من حتی از رویداد RowEditEnding در شی DataGrid هم استفاده کردم . این رویداد ، قبل از اینکه متد Validate (برای اعتبارسنجی) اجرا میشه اما دقیقا مثل متد Validate ، اطلاعاتی که کاربر برای اون سطر را (در رویداد RowEditEnding و در متغییر رویدادیِ e.Row.Item) بهم نمیده . بلکه این هم اطلاعات همون سطری که فعلا در پروپرتیِ Persons در کلاس PhoneBook هست را میده که اون اطلاعات اصلا ملاکم نیست و به دردم نمیخوره .

برای شما هم این رویداد ، این طوره؟
یعنی زمانی که کاربر مقادیر سطری را ویرایش یا اضافه کرد ، نشون نمیده؟

------

بعد هم در همون Binding ، پروپرتیِ UpdateSourceTrigger را روی مقدار Explicit گذاشتم :



ItemsSource="{Binding UpdateSourceTrigger=Explicit}"


تا خودم با فراخونی متد BindingExpression.UpdateSource ، بعد از اینکه اطلاعات وارد شده ی کاربر را گرفتم ، اون متد را فراخوانی کنم تا Binding اش ، مقدار Target Object را برای Source Object ارسال کنه اما به احتمال زیاد چون (از طرف DataGrid) ، مقدار Binding را به کل کالکشن انجام دادم اما از اون طرف ، ,وقتی ، سطری اضافه یا ویرایش میشه ، کلِ شیِ کالکشن در Binding Source Object ، تغییری نمیکنه بلکه فقط سطرها که همون آیتم های اون کالکشن هستند ، تغییر میکنن ، احتمالا به همین دلیل تغییر در مقدارِ پروپرتیِ UpdateSourceTrigger ام تاثیری در رفتارش نداره .

=============

نهایتا اینکه دوستان به نظرتون چی کار کنم؟
فرضا اگه میشه ، توی همون متد Validate یا اگه نشد ، حتی در رویداد RowEditEnding بتونم به مقداری که کاربر ویرایش یا اضافه کرد ، دسترسی پیدا کنم؟
یا روش های دیگه برای اعتبار سنجی ، چه راهکاری پیشنهاد میدین؟

دلیلش چیه که در متد Validate یا در رویداد RowEditEnding ، اطلاعاتی که کاربر ویرایش کرد را نمیده و آیا برای شما هم این دو تا متد و رویداد ، همینطوره؟

و همچنین راهکارتون چیه؟

------

روش پیاده سازی اینترفیس IDataErrorInfo را برای کالکشن انجام دادم اما هم اصلا اجرا نشد و اگه هم اجرا بشه ، نمیدونم در اون حالت برای کدوم آیتمِ کالکشن ، تغییرات اتفاق میافته .

- روش دیگه اینکه بذارم اول ، Binding Source Object و کلا آیتم های کالکشن ام ، اول تغییرات روشون اِعمال بشن و بعد اعتبارسنجی انجام بشه (مثل همونی که در لینک مایکروسافت گفت و مقدار پروپرتی ValidationStep ئه ValidationRule (در کد من ، همون کلاس DataGridItemsSource_Persons_ValidationRule) را به مقدار UpdatedValue تغییر بدم تا این اتفاق بیفته) و بعد بیام اعتبار سنجی را انجام بدم

که در اون صورت ، هم زیاد کارایی نداره و هم اینکه خوب همین کار را بدون اعتبار سنجی هم میشه انجام داد . یعنی در لایه ی Model میشه هر وقت آیتم کالکشن مون تغییر کرد ، با خبر بشیم و این کار را انجام بدیم . البته یه کم سخت تره . و هم اینکه در این صورت ، اگه اعتبار سنجی معتبر نبود ، دوباره اون آیتم را باید برگردونیم به مقدار قبلی اش که فکر کنم با پیاده سازی اینترفیس IEditableObject ، این کار قابل انجام باشه .

این روش انگار نسبت به روش های دیگه ، کاراتر به نظر میرسه اما خوب باز هم کارایی اش کمتر (نسبت به اینکه اگه جواب سئوالم را پیدا کنم) و هم کدهاش پیچیده تر میشه .

روش دیگه ، شاید از Converter ها بشه استفاده کرد (تست نکردم) اما اگه هم بشه ، اصولی نیست .

تشکر دوستان .

SajjadKhati
یک شنبه 07 اسفند 1401, 20:20 عصر
سلام
داستان این هست که DataGrid که 2 حالتِ در حال ویرایش و غیر ویرایش (فقط نمایش اطلاعات) داره .
در حال ویرایش ، زمانی هست که کاربر داره مقداری را وارد یا عوض میکنه .
بصورت پیش فرض ، وقتی کاربر ، شروع به ویرایش میکنه ، یه کنترل TextBox درونِ اون سلول ایجاد میشه که وقتی از این حالت خارج بشه (با زدن کلید Enter یا هر چیز دیگه) ، دیگه این TextBox وجود نداره که مقدارش در دسترس باشه .

مقدارِ این TextBox که شامل اطلاعاتی که کاربر ، ویرایش کرد ، هست ، نهایتا فقط تا زمانی که رویدادِ CellEditEnding ئه DataGrid اجرا بشه (توسط متغییرِ رویدادیِ e.EditingElement که بصورت پیش فرض ، اگه خودمون ControlTemplate ای ندیم ، از نوع TextBox هست) ، در دسترس هست . یعنی نهایتا تا همون موقع و همون رویداد میتونیم متوجه بشیم که کاربر ، چه اطلاعاتی را وارد و یا ویرایش کرد .

یعنی حتی در رویداد RowEditEnding هم این اطلاعات دیگه در دسترس مون نیست .
CellEditEnding ، زمانی هست که کاربر ، یک سلول را ویرایش کرد و کارش تمام شد (با unfocuse شدنِ سلول و هر جور دیگه ای) .
RowEditEnding هم زمانی که ویرایش یک سطر ، به اتمام رسید .
هر دو رویداد ، قبل از اجرای متد Validate برای اعتبارسنجی ، اجرا میشن .

بنابراین یکی از راهکارها اینه که تا زمانی که درون رویداد CellEditEnding هستیم ، چون به متد Validate ، مقدارِ DataGrid.Items.CurrentItem مون (که در مثال بالا همون شیِ Person هست) ، ارسال میشه (به عنوان مقدار پارامتر value که از نوع BindingGroup و عضوِ Items[0] اش) ، پس اعضای این CurrentItem را به مقداری که کاربر درون اون TextBox قرار میده ،تغییر بدیم .

------

یا بهتر اینکه (برای اینکه پروپرتیِ CurrentItem تغییری نکنه چون ممکنه یه جای دیگه مشکل ساز بشه و ندونیم) ، در این رویدادِ CellEditEnding ، اطلاعات را بگیریم و ذخیره کنیم و در زمانی که در رویداد RowEditEnding هستیم ، اون را برای متغییر یا پروپرتی استاتیک ای که در کلاس DataGridItemsSource_Persons_ValidationRule تعریف کردیم ، بفرستیم و بعد ، مقدار اون عضو استاتیک را در متد Validate ، زمانی که عملیات با موفقیت انجام شد (یعنی سر آخر) قبل از اینکه از متد خارج بشیم ، null کنیم .


البته برای اینکه MVVM نقض نشه ، میشه در کلاسی که از ValidationRule ارث میبره (در اینجا کلاس DataGridItemsSource_Persons_ValidationRule) ، متدهای استاتیکی ساخت و وظایف مقداردهی ها را توسط این متدها به لایه ی ViewModel منتقل کرد (چون این کلاس در لایه ی ViewModel هست) ، پس روش زیر پیشنهاد میشه :

در MainWindow.xaml :



<Window x:Class="WPF_Practice_2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_Practice_2"
xmlns:localClasses="clr-namespace:WPF_Practice_2.Classes"
xmlns:bindingValidations="clr-namespace:WPF_Practice_2.Classes.BindingValidation s"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">


<Window.Resources>


<localClasses:ViewModel x:Key="ViewModelKey"/>


<CollectionViewSource x:Key="PersonsCollectionViewSourceKey"
Source="{Binding
Source={StaticResource ViewModelKey},
Path=PhoneBook.Persons}"/>

<ControlTemplate TargetType="{x:Type Control}"
x:Key="PersonValidationRulesControlTemplateKey">
<Grid Margin="0,-2,0,-2" Background="Transparent"
ToolTip="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type DataGridRow}},
Path=(Validation.Errors)[0].ErrorContent}">


<Ellipse StrokeThickness="0" Fill="Red" Width="{TemplateBinding FontSize}"
Height="{TemplateBinding FontSize}"/>
<TextBlock Text="!" FontSize="{TemplateBinding FontSize}" FontWeight="Bold"
Foreground="White" HorizontalAlignment="Center" />


</Grid>
</ControlTemplate>


</Window.Resources>



<Grid DataContext="{Binding
Source={StaticResource PersonsCollectionViewSourceKey}}">


<DataGrid Name="PhoneBookDataGrid" HorizontalAlignment="Center" Height="271"
Margin="0,153,0,0" VerticalAlignment="Top" Width="780"
AutoGenerateColumns="False"
ItemsSource="{Binding ValidatesOnExceptions=True}"
RowValidationErrorTemplate="{StaticResource PersonValidationRulesControlTemplateKey}"
CellEditEnding="PhoneBookDataGrid_OnCellEditEnding"
RowEditEnding="PhoneBookDataGrid_OnRowEditEnding"
AlternationCount="{Binding Count}">


<DataGrid.RowValidationRules>
<bindingValidations:DataGridItemsSource_Persons_Val idationRule/>
</DataGrid.RowValidationRules>


<DataGrid.Columns>
<DataGridTextColumn Header="ردیف" IsReadOnly="True"
Binding="{Binding AlternationIndex,
RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>


<DataGridTextColumn Header="شناسه"
Binding="{Binding Path=PersonID, ValidatesOnExceptions=True}"/>


<DataGridTextColumn Header="نام"
Binding="{Binding Path=FirstName, ValidatesOnExceptions=True}"/>


<DataGridTextColumn Header="نام خانوادگی"
Binding="{Binding Path=LastName, ValidatesOnExceptions=True}"/>


<DataGridTextColumn Header="شماره تماس"
Binding="{Binding Path=PhoneNumber, ValidatesOnExceptions=True}"/>


<DataGridTextColumn Header="کد ملی"
Binding="{Binding Path=NationalCode, ValidatesOnExceptions=True}"/>
</DataGrid.Columns>


</DataGrid>

</Grid>
</Window>




در MainWindow.xaml.cs :



public partial class MainWindow : Window
{
/// <summary>
/// برای فلگ کردن اینکه وقتی که رویداد RowEditEnding میرسد ، آیا قبل از آن ، سلول مربوط به آن سطر جاری تغییر کرد که باعث اجرای این رویداد شد
/// تا برای اعتبار سنجی ارسال شود یا نه .
/// </summary>
private bool doesCurrentRowCellChangeFlag = false;








private void PhoneBookDataGrid_OnCellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
if (this.doesCurrentRowCellChangeFlag == false)
this.doesCurrentRowCellChangeFlag = true;


DataGridItemsSource_Persons_ValidationRule.CellEdi tEnding(this.PhoneBookDataGrid.Items.CurrentItem,
e.Column.SortMemberPath, ((TextBox) e.EditingElement).Text);
}




private void PhoneBookDataGrid_OnRowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
/// ممکنه این رویداد ، علاوه بر زمان ویرایش سلول ، مجددا در زمانی که سلول را که ویرایش کرده بودیم و فوکوس را روی یه کنترل دیگه میخوایم منتقل کنیم ،
/// مجددا این رویداد اجرا بشه . این شرط ، برای جلوگیری از اجرای این رویداد در اون موقع هست تا عمل binding را به model منتقل نکنه .
if (this.doesCurrentRowCellChangeFlag == false)
{
e.Cancel = true;
return;
}


DataGridItemsSource_Persons_ValidationRule.RowEdit Ending();
this.doesCurrentRowCellChangeFlag = false;
}






}




---

در کلاس DataGridItemsSource_Persons_ValidationRule که در لایه ی ViewModel باشه (نوع کلاس Person ، در لایه ی Model تعریف شده) :




public class DataGridItemsSource_Persons_ValidationRule : ValidationRule
{
private static Person editValidatePerson = null;


private static Person tempEditValidatePerson = null;








public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
/// اگر مقدار EditValidatePerson برابر با null بود ، پس یعنی اعتبار سنجی ، یکطرفه و از طرف Model داره به View منتقل میشه ، پس همه را معتبر بزنه .
if (editValidatePerson == null)
return ValidationResult.ValidResult;


if (editValidatePerson.PersonID < 0)
return new ValidationResult(false, "شناسه باید عددی مثبت باشد");


/// فقط در این قسمت ، یعنی در صورت موفقیت آمیز بودن عمل ثبت ، null میکنیم چون اگر موفقیت آمیز نباشد ، اطلاعات پاک نشود
/// و امکان ویرایشِ مجددِ آخرین اطلاعاتِ وارد شده ، وجود داشته باشد .
tempEditValidatePerson = null;
editValidatePerson = null;
return ValidationResult.ValidResult;
}






/// <summary>
/// نوع پارامتر person باید از نوع Person باشد .
/// برای این اینجا obnect تعریف شده چون برای ارسال از View به اینجا ، نیاز به تبدیل نباشد تا MVVM نقض شود .
/// </summary>
public static void CellEditEnding(object currentRowPerson, string editedPersonItemPropertyName, string editedPersonItemValue)
{
Person tempPerson = currentRowPerson as Person;
if (tempPerson == null)
throw new Exception("person");


if (tempEditValidatePerson == null)
{
tempEditValidatePerson = new Person(tempPerson.PersonID, tempPerson.FirstName, tempPerson.LastName,
tempPerson.PhoneNumber, tempPerson.NationalCode);
}


if (editedPersonItemPropertyName == "PersonID")
{
int personID = -1;
if (int.TryParse(editedPersonItemValue, out personID) == true)
tempEditValidatePerson.PersonID = personID;
}
else if (editedPersonItemPropertyName == "FirstName")
tempEditValidatePerson.FirstName = editedPersonItemValue;
else if (editedPersonItemPropertyName == "LastName")
tempEditValidatePerson.LastName = editedPersonItemValue;
else if (editedPersonItemPropertyName == "PhoneNumber")
{
int phoneNumber = -1;
if (int.TryParse(editedPersonItemValue, out phoneNumber) == true)
tempEditValidatePerson.PhoneNumber = phoneNumber;
}
else if (editedPersonItemPropertyName == "NationalCode")
tempEditValidatePerson.NationalCode = editedPersonItemValue;
}




public static void RowEditEnding()
{
editValidatePerson = tempEditValidatePerson;
}




}


ولی واقعا این بدیهی هست که باید DataGrid جوری طراحی میشد که در متد Validate ، اطلاعاتی که قراره ثبت بشه را در اختیارمون بذارن .
این چه طراحی ای بود ، تعجب میکنم!!