Sunday 19 December 2010

Entity Framework 4 “Code First”

Все почалось з того що я вирішив освіжити свої знання в ASP MVC і заодно спробувати новий енжін разор про який я недавно писав, скажу чесно про це я не жалію і рекомендую вам ознайомитись з його синтаксисом, а ще краще спробувати написати якийсь семпл. Отож ознайомлення пройшло як ви вже зрозуміли успішно і мені захотілось написати щось більше а ніж просто стандартну CRUD(Create, Read, Update, Delete) аплікацію, для цього мені була необхідна база даних в якій я б міг зберігати дані. Спочатку вибір автоматично впав на Entity Framework  з яким я якийсь час працював, але знаючи про його нову фічу “Спочатку код” вирішив що необхідно об’єднати приємне з корисним і вивчити попутньо щось нове.

Спочатку Код!

Принцип дуже простий і він в той же час досить необхідний для початківців, їм необхідна база даних для зберігання даних і на ранніх етапах без певного досвіду важко спроектувати її такою якою б вона могла проіснувати до фіналу проекту. Кожного дня зявляються ідеї і необхідно щось підправити або додати нову фічу. Для людей не дуже добре знайомих з SQL це може стати проблемою та й просто займати трохи більше часу ніж хотілось би на це витрачати, тому ми створюємо прості класи які описують обєкти з якими ми хотілиб  працювати а база буде згенерована автоматично. В даному пості ми створимо заготовку аплікації і надалі попробуємо її вдосконалити, першим кроком для нас буде створення моделі бази даних за допомогою класу.

Наприклад ось моя “база даних”:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
using System.Web;
namespace Portal.Models
{
    public class Photo
    {
        [Key]
        public int PhotoID { get; set; }
        public int PlaceID { get; set; }
        public string PhotoUrl { get; set; }
        public string PhotoTitle { get; set; }
        public string PhotoInfo { get; set; }
        public string PhotoDateTime { get; set; }
        public int Rating { get; set; }
    }
    public class Comment
    {
        [Key]
        public int CommentID { get; set; }
        public int PhotoID { get; set; }
        public int PlaceID { get; set; }
        public string UserName { get; set; }
        public string CommentText { get; set; }
        public int Rating { get; set; }
        public DateTime CommentDate { get; set; }
        public string UserMail { get; set; }
    }
    public class Place
    {
        [Key]
        public int ID { get; set; }
        public string Name { get; set; }
        public string Oblast { get; set; }
        public string City { get; set; }
        public string Street { get; set; }
        public string Building { get; set; }
        public string Phone { get; set; }
        public string PhoneReception { get; set; }
        public string Descrption { get; set; }
        public string Location { get; set; }
        public virtual ICollection<Comment> Comments { get; set; }
        
        public virtual ICollection<Photo> Photos { get; set; }
        public string Logo { get; set; }
    }
    public class PortalEntities : DbContext
    {
        public DbSet<Place> Places { get; set; }
        public DbSet<Photo> Photos { get; set; }
        public DbSet<Comment> Comments { get; set; }
    }
}

Незважаючи на великий обсяг тексту це лише звичайні властивості з необхідного нам типу які відображають об’єкти Place Photo i Comment. Останній клас PortalEntities батьківським класом для якого є DbContext зробить всю необхідну нам роботу. В ньому описано три властивості типу DbSet<T> які будуть згодом представляти три таблиці з відповідними іменами. Тепер перейдемо до безпосереднього прикладу в якому ми це все заставимо працювати, для цього нам буде необхідно створити новий проект :

image

на наступному вікні я обрав пустий MVC 3 проект і в якості Razor в якості вюенжіна, в студії ми повинні побачити наступне:

Снимок

Наступний крок це додавання винуватця статті до проекту, для цього я використаю NuGet, так буде скорше але якщо Microsoft ADO.NET Entity Framework Feature Community Technology Preview 4 у вас вже встановлений то ви можете просто добавити на нього референси. Для встановлення за допомогою нугету нам необхідно ввести наступну команду:

PM> Install-Package SQLCE.EntityFramework

Після чого добавляємо в папку Models вище наведену модель PortalEntities. Тепер знову ж за допомогою NuGet швдиенько згенеруємо контроллер та сторінки, для цього спочатку наш павер шелл зробимо трохи розумнішим встановивши MvcScaffold:

PM> Install-Package MvcScaffold

А після цього виконаємо наступну команду:

PM> Scaffold-Controller -m Place -c PortalEntities

Перший параметр вказує на те що ми хочемо створити контролер та сторінки для роботи з об’єктом Place, другий параметр вказує на контекст через який ми будемо працювати з Place.

02

Вище наведені стрічки свідчать про те що нам в проект було добавлено один контроллер і декілька сторінок.

Код контролера цілком стандартний тому я його повністю наводити не буду а лише те що нас цікавить зараз:

 public class PlaceController : Controller
    {
        private PortalEntities context = new PortalEntities();
        //
        // GET: /Places/
        public ViewResult Index()
        {
            return View(context.Places.ToList());
        }
        //
        // GET: /Places/Details/5
        public ViewResult Details(int id)
        {
			Place p = context.Places.Single(x => x.ID == id);
            return View(p);
        }
      ................

Як видно з екшина деталей ми можемо звертатись до контексту за допомогою звичайного лінкю виразу. Що спрощує нам життя. Також надалі коли ми додано реальну базу даних нам не прийдеться тут взагалі нічого міняти, все що нам потрібно це прописати стрічку до БД. та це буде пізніше а поки що…

В файлі Global.asax нам необхідно змінити дефолтний контролер “Home” на наш “Place” :

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.MapRoute(
        "Default", // Route name
        "{controller}/{action}/{id}", // URL with parameters
        new { controller = "Place", action = "Index", id = UrlParameter.Optional } // Parameter defaults
    );
}

Після компіляції та запуску ми отримаємо наступне повідомлення:

03

Не те що ми хотіли отримати… Діло тут в тому що в нас немає папки App_Data а саме там має бути створений наш файл бази даних. Через контекстне меню проекту вибираємо:

04

І нарешті отримуємо хоч якийсь результат:

05

Натискаємо лінк Create New і переходимо до створення нового об’єкту:

06

Вводимо дані і відповідно отримуємо результат:

07

Також нам доступна функціональність редагування, видаляння і перегляду деталей. Реально оцінивши час на створення цього проекту можна зробити висновок що це не більше 5 хв. і то 4 з яких пішли на створення моделі БД. В недавно створену папку добавився файл бази даних:

08

В цьому файлі міститься тільки що введена та збережена нами інформація. При цьому якщо ми зараз знову запустимо наш проект то отримаємо повідомлення про помилку “File already exists. Try using a different database name. [ File name = c:\users\bats\documents\visual studio 2010\Projects\MvcApp\MvcApp\App_Data\MvcApp.Models.PortalEntities.sdf ]” Таке повідомлення ми отримали тому що наша база даних вже існує а проект по замовчуванню хоче створити нову і .. Як обіцяє команда розробників це все буде виправлено в фінальній версії, а поки що маємо те що маємо.

Для виправлення даної ситуації можна скористатись двома варіантами

  • Переходимо до файлу AppStart_SQLCEEntityFramework.cs та виправляємо стрічку
 Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");

На

Database.SetInitializer( new RecreateDatabaseIfModelChanges<PortalEntities>());

В файлі Web.config вставляємо конекшин стрінг:

  <connectionStrings>
    <add name="PortalEntities" 
         connectionString="Data source=|DataDirectory|MvcApp.Models.PortalEntities.sdf"
         providerName="System.Data.SqlServerCe.4.0"/>
  </connectionStrings>

Знову запускаємо аплікацію перший раз вона видасть те ж саме повідомлення а далі буде працювати.

Це дозволить перестворювати базу даних кожен раз коли в нас змінилась модель, старі дані при цьому будуть втрачені, а Якщо модель не змінилась то дані відповідно втрачені не будуть.

  • Також можна використати метод який продемонстрував Скот Хенселман у своєму пості. Дуже рекомендую прочитати його якщо знання англійської дозволяють.
Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0",
HostingEnvironment.MapPath("~/App_Data/"), "");

Використавши його в Web.config вставляти вже нічого не треба але після зміни моделі необхідно знову замінити вище наведену стрічку на ту що була по замовчуванню:

Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");

Отож ми вдало підключили базу до свого проекту і навіть змогли обробляти дані надалі ми попробуємо більш детально ознайомитись з фічами та правилами використання даного принципу, вдосконалимо нашу аплікацію.

Джерела :

Програма:

Sunday 10 October 2010

Провірка даних в WPF (Simple Wpf Validation)

Сьогодні я покажу вам як легко можна валідувати дані введені користувачем. Для цього ми створимо простенький клас і поексперементуємо з ним. Першим буде розглянуто метод валідаціі на основі ексепшинів, після чого ми перейдемо до валідаціі яка появилась починаючи з фреймворка 3,5, валідаціі на основі IDataErrorInfo. Я пропущу створення проекту так як це вже було описано в попередніх постах і перейду безпосередньо до опису методу та реалізації.

Валідація на основі Exceptions.

Цей метод передбачає, що дані для користувацького інтерфейсу беруться з властивостей певного обєкту і відповідно коли вони змінюються на формі, вони автоматично записуюються в властивість обєкта, а так як записати в властивість типу int можна лише значення int то після того як користувач ввів щось типу “Десять” виникає помилка що дані не можуть бути записані і це відобразиться на нашому інтерфейсі. Такий же метод валідації існує в ASP NET. маю надію розглянути його роботу і там. Описав ніби правильно але в такому разі легше показати на прикладі:

Window1.xaml:

<Window x:Class="WpfValidation.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="400">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="35" ></RowDefinition>
            <RowDefinition Height="35" ></RowDefinition>
            <RowDefinition Height="35" ></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50"></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>
        
        <TextBlock Grid.Row="0" Grid.Column="0" Margin="5">Name:</TextBlock>
        <TextBlock Grid.Row="1" Grid.Column="0" Margin="5">Age:</TextBlock>
        <TextBlock Grid.Row="2" Grid.Column="0" Margin="5">Sex:</TextBlock>
        
        <TextBox Name="name" Grid.Row="0" Grid.Column="1" Width="200" Margin="5" Text="{Binding Path=Name, ValidatesOnExceptions=true, NotifyOnValidationError=true}"></TextBox>
        <TextBox Name="age" Grid.Row="1" Grid.Column="1" Width="200" Margin="5" Text="{Binding Path=Age, ValidatesOnExceptions=true, NotifyOnValidationError=true}"></TextBox>
        <TextBox Name="sex" Grid.Row="2" Grid.Column="1" Width="200" Margin="5" Text="{Binding Path=Sex, ValidatesOnExceptions=true, NotifyOnValidationError=true}"></TextBox>
                
    </Grid>
</Window>

В редакторі це виглядатиме наступним чином:

01

Window1.xaml.cs:

using System.Windows;
namespace WpfValidation
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            loadData();
        }
        private void loadData()
        {
            UserItem item = new UserItem{
                              Name = "Ihor",
                              Age = 24,
                              Sex = UserItem.SexEnum.Male
                          };
            name.DataContext = item;
            age.DataContext = item;
            sex.DataContext = item;
        }
    }
    internal class UserItem
    {
        internal enum SexEnum
        {
            Male,
            Famale
        }
        public string Name { get; set; }
        public int Age { get; set; }
        public SexEnum Sex { get; set; }
    }
}
 image

Підсвітка спрацьовує автоматично при зміні фокусу, але якщо ми добавимо до стрічки з біндінгом наступний фрагмент “UpdateSourceTrigger=PropertyChanged“ то дані валідуватимуться зразу при введенні. Остаточний код xaml файлу наступний:

<Window x:Class="WpfValidation.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="400">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="35" ></RowDefinition>
            <RowDefinition Height="35" ></RowDefinition>
            <RowDefinition Height="35" ></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50"></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>
        
        <TextBlock Grid.Row="0" Grid.Column="0" Margin="5">Name:</TextBlock>
        <TextBlock Grid.Row="1" Grid.Column="0" Margin="5">Age:</TextBlock>
        <TextBlock Grid.Row="2" Grid.Column="0" Margin="5">Sex:</TextBlock>
        
        <TextBox Name="name" Grid.Row="0" Grid.Column="1" Width="200" Margin="5" Text="{Binding Path=Name, ValidatesOnExceptions=true, NotifyOnValidationError=true, UpdateSourceTrigger=PropertyChanged}"></TextBox>
        <TextBox Name="age" Grid.Row="1" Grid.Column="1" Width="200" Margin="5" Text="{Binding Path=Age, ValidatesOnExceptions=true, NotifyOnValidationError=true, UpdateSourceTrigger=PropertyChanged}"></TextBox>
        <TextBox Name="sex" Grid.Row="2" Grid.Column="1" Width="200" Margin="5" Text="{Binding Path=Sex, ValidatesOnExceptions=true, NotifyOnValidationError=true, UpdateSourceTrigger=PropertyChanged}"></TextBox>
                
    </Grid>
</Window>

Тепер про використані властивості з біндінгом

UpdateSourceTrigger=PropertyChanged Вказує коли саме проводити валідацію. Детальніше тут http://msdn.microsoft.com/ru-ru/library/system.windows.data.binding.updatesourcetrigger.aspx
ValidatesOnExceptions В даному випадку є основним і вказує чи робити валідацію данних при ексепшинах.
NotifyOnValidationError Вказує на необхідність інформування при виникненні помилок при валідацї.

На цьому перший крок у валідації даних я вважаю завершеним, тепер ми переходимо до розгляду методу на основі IDataErrorInfo.

Валидация з використанням інтерфейсу IDataErrorInfo

Для реалізації даного методу нам необхідно реалізувати цей інтерфейс в нашому класі, він складається з індексатора і властивості. Властивість повертає стрічку з помилкою для всього класу а індексатор вказує на конкретну властивість в якій виникла помилка валідації, якщо помилок немає то відповідно повертається null. Виглядає цей інтерфейс наступним чином :

  public interface IDataErrorInfo 
  {
     string Error { get; }
     string this[string propertyName] { get; }
  }

Давайте попробуємо реалізувати цей інтерфейс на нашому прикладі:

public class UserItem : IDataErrorInfo
    {
        public enum SexEnum
        {
            Male,
            Famale
        }
        public string Name { get; set; }
        public int Age { get; set; }
        public SexEnum Sex { get; set; }
        public string this[string columnName]
        {
            get
            {
                string msg = null;
                switch (columnName)
                {
                    case "Name":
                        if (string.IsNullOrEmpty(this.Name))
                            msg = "Name can't be empty.";
                        break;
                    case "Age":
                        if (this.Age <= 0)
                            msg = "Age can't be less then 0.";
                        break;
                    case "Sex":
                        if (this.Sex != SexEnum.Famale && this.Sex != SexEnum.Male)
                        {
                            msg = "Incorect Sex.";
                        }
                        break;
                    default:
                        throw new ArgumentException(
                          "Unrecognized property: " + columnName);
                }
                return msg;
            }
        }
        public string Error
        {
            get
            {
                return null;
            }
        }
    }

В даному випадку ми використали індексатор, але нічого не мішає нам використати властивість Error. Після запуску проекту ви побачите що ніякої валідації не відбувається і це не дивно тому що наш хамл поки що не знає про можливість валідації даних, тому добавимо до нього властивості які дозволять використовувати валідацію:

<Window x:Class="ValidationApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="35" ></RowDefinition>
            <RowDefinition Height="35" ></RowDefinition>
            <RowDefinition Height="35" ></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50"></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Margin="5">Name:</TextBlock>
        <TextBlock Grid.Row="1" Grid.Column="0" Margin="5">Age:</TextBlock>
        <TextBlock Grid.Row="2" Grid.Column="0" Margin="5">Sex:</TextBlock>
        <TextBox Name="name" Grid.Row="0" Grid.Column="1" Width="200" Margin="5" Text="{Binding Path=Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"></TextBox>
        <TextBox Name="age" Grid.Row="1" Grid.Column="1" Width="200" Margin="5" Text="{Binding Path=Age, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"></TextBox>
        <TextBox Name="sex" Grid.Row="2" Grid.Column="1" Width="200" Margin="5" Text="{Binding Path=Sex, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"></TextBox>
    </Grid>
</Window>

Я добавив наступні властивості :

  • ValidatesOnDataErrors=True
  • UpdateSourceTrigger=PropertyChanged

Перша для того щоб використати нашу валідацію, а друга для визначення часу її спрацьовування, в даному випадку після зміни тексту. Детальніше про ці та інші параметри на сайті майкрософта: http://msdn.microsoft.com/en-us/library/system.windows.data.binding.updatesourcetrigger.aspx

На остаток хотів би показати як можна програмно встановити контрол в стан помилки валідації, це може допомогти вам коли ви будете розробляти свою систему валідації (хоча я рекомендую використовувати стандартні методи так як в більшості випадків їх можливостей вистачає з головою). Для цього нам будуть потрібні два методи

  • Validation.MarkInvalid
  • Validation.ClearInvalid

Для включення стану помилки валідації:

Validation.MarkInvalid(
   name.GetBindingExpression(TextBox.TextProperty),
   new ValidationError(new ExceptionValidationRule(),
   name.GetBindingExpression(TextBox.TextProperty)));

Для очищення:

Validation.ClearInvalid(name.GetBindingExpression(TextBox.TextProperty));

Ці методи дозволять вам підсвітити валідацію на контролах відповідно до встановленого вами шаблону або шаблону заданого по замовчуванню.

Всім поменше помилок.

Source Code