Декілька моїх останніх тестових проєктів.
роботодавці дуже люблять різноманітні тести для своїх майбутніх робітників. В чому загальна помилка оцінки робітника по результатам тестів - подивимося на це детальніше.
- По-перше тести ніколи не відповідають саме тієї роботи, яку робітнику доведеться виконувати у майбутньому. Ось наприклад один з тестів, який я пройшов (невдало) для працевлаштування у компанію VmWare. Що це за маячня? Якийсь мавпи розкладають якісь кільця...
При тому, що я використовую VmWare вже мабуть 20-ть років, розумію усі недоліки і розумію, як можна цей софт покращити. Ось невеличке відображення моєї щоденної роботи у реальному світі - Основы работы с VmWare, Модернизация Web-сервера - долой платный софт и Hyper-V. - це лише невеличке відображення у моєму блозі реального світу, яке взагалі це має відношення до питань, які ставить мені ця фірма при працевлаштуванні?
- По-друге, тести ігнорують все попереднь життя людини, весь його акумульований досвід за все попереднь життя. Ну наприклад моє перше місце роботи адміном було 30 років тому. І далі було дуже-дуже багато таких робіт, ну ось наприклад я працював адміном ІМПЄ. Ну наприклад, якийсь хлопчинка пройде ці тести з мавпами, а я ні. Так що він зможе працювати адміном краще за мене? Ці тести, можливо мають якусь користь серед учнів, які є клони один одного, щоб виявити нахил до навчання. Але який сенс порівнювати цими тестами людину, яка має 30 років опиту роботи і маленьку дитинку? Це ж повна дурниця!
Тому, ось такі опросники я вважаю більш корисними:
Вибачайте, друзі, за велику кількість граматичних помилок у моїх відповідях, все це робиться у жорсткому режиму кризи, коли часу не вистачає зовсім - тому помилок так багато. - Третя дурниця працевлаштування на основі тестів у тому, що рекрутери намагаються відібрати людину, яка працювала з якоюсь конкретної бібліотекою. Ну от наприклад, на SouceForge.NET зараз лежить 18463 сторінок з переліком різноманітних бібліотек. На кожній сторінці їх по 30, тобто на одному лише сайті у інтернеті викладено більш ніж ПІВМІЛЬОНА різноманітних OpenSource бібліотек. Зрозуміло, що загальна кількість різноманітних бібліотек (включно проприєтарні, тобто код яких не належить комусь і не викладається публічно) налічу. у тисячі разів більше. Ну і який сенс питати на співбесіді - як добре ви знаєте, наприклад бібліотеку Rx для паралельного Linq. Да, я колись ії використовував один чи два раза (ось тут, навіть, якийсь приклад з нею лежить Yield - і ці люди забороняли нам багато років колупатися у носі?. Використав і через п'ять хвилин забув про неї. Навіщо мені про неї пам'ятати. Буде потрібно, згадаю знов. І на роботі у цієї фірмі, якщо навіть вона і потрібна, але це не все що потрібно! Це лише невеличка особливість асинхронізму. Але рекрутери можуть впитися, як піявки у якусь з мільонів бібліотек - а що ти пам'ятаєш, наприклад, як там звільнюється пам'ять по Dispose. Взагалі нічого не пам'ятаю, відвідаю я. А як мені це буде потрібно - прочитаю у інтернеті!
- Четверта дурниця тестів в тому, що для кваліфікованих програмістів і адміністраторів тестери, які проводять співбесіди - виглядають дурнями. Повними дурнями. Ось наприклад я колись розмовляв з Anrijs Vitolins (skype vitamins.lv) - він проводив співбесіду на мою спробу працевлаштування у якусь Ріжську софтверну фірму. Він спитав мене, що мені більше подобається - Linq-to-SQL чи EntityFramework. Я відповів йому, що EntityFramework працює занадто повільно, порівняно з Linq-to-SQL, не підтримує самих базових можливостей MS SQL, таких як NOLOCK, View і взагалі пристосован для учбових сайтів, більше для навчання, ніж для реального використання у навантажених проєктах. Умови використання EntityFramework включають можливості повної зміни двигуна SQL-серверу, тобто с PostgreSQL, з MySQL і з MS SQL ця бібліотека працює повністю однаковим чином, використовуючи їх лише самим поверховим образом, як сховище табличок. Про яку швидкість при використанні цієї бібліотеці може йти мова, якщо ща бібліотека звертається до MySQL та MS SQL однаковими командами? Цей хлопчина почув мій спітч щодо умов використання цієї нової бібліотеки доступа до даних та припинив співбесіду. Тобто, зрозуміло, що він прочитав у своєму житті тільки одну книжку - і це була рекламна книжка про EntityFramework, у якої цього не було написано. Він взагалі не розуміє для чого була розроблена бібліотека EntityFramework.DLL додатково к десятку інших бібліотек доступа до даних, взагалі не розуміє головного завдання розробки цієї бібліотеці - змінювати SQL-сервер у проєкті швидко та просто - сьогодні працюємо на Oracle, завтра на MySQL, після завтра на PstgreSQL. Хлопчинка на розуміє, що якщо SQL-сервер на протязі життя проэкту змінюватися не буде - ы це буде постійно MS SQL - то для використання MS SQL у пакеті .NET Framework існують спеціальні бібліотеці (найкраща з них Linq-to-SQL), які використовують багато специфічних можливостей SQL-серверу (ну для початку розуміють що таке Вьюхи). Але якщо потрібно працювати ще якісніше, ще швидше - то і Linq-to-SQL не підійде. Про що розмовляти далі з дитиною, яка прочитала у своєму житті одну рекламну книжку про EntutyFramework (і мабуть вже написала свої перші у житті сто стрічок коду).
- П'ята дурниця полягає у тому, що для тестів з мільонів, викладених у мережі бібліотек, работодавці відбирають самі найновіші і самі найменьш поширені бібліотеки. Ну наприклад замість jQuery вибирають React. Трошки вдумайтесь у це - у цілому світі існує лише 527 сайтів з React. І мільони сайтів зроблені, наприклад, на jQuery. А існує ще й бібліотека Angular, яка взагалі не радикально відрізняється від React, але який взагалі проштовхує на ринок Google (топ один грошовий чувал у світі, понад 500 мільярдів долларів). Але де ви бачили тести для працевлаштування на jQuery? Вони тільки на React чи на Angular! І від мене у цьому тесті захотіли сайт саме на React. Кому взагалі потрібна технологія, яку при такої агресивної рекламі (75 рекламних агенцій розкручує цю технологію) - взагалі вдалося використати лише на 500 сайтах у всьому світі. А з іншого боку технології, які взагалі не мають рекламного бюджету та агресивних рекламних компаній - використовуються на десятках мільонів сайтів!
Далі, друзі, я покажу три свої останні теста, який я зробив, намагаючись знайти нормальну (не фрілансерську) роботу.
- 1. Тест для компанії thehouse.bg (VB, MVC 4, Razor, Linq-to-SQL)
- 2. Тест для компанії bobs.bg (C#, MVC 5, AJAX, EntityFramework)
- 3. Тест для компанії Sigma (C#, MVC 5, AJAX, Ninject, EntityFramework, WebAPI2, DTO)
1. Тест для компанії Thehouse.bg (VB, MVC 4, Razor, Linq-to-SQL)
Тест був мною виконаний повністю, але пропозицію про постійну роботу я так і не отримав від них. У інтернеті код теста я виклав ось тут http://thehouse.vb-net.com/. Як бачите, сайт чудово працює. Цей сайт трошки нагадує мій реальний проєкт shel-auto.ru, яким я займався декілька років, тільки у реальному проєкті більше 50-ти тисяч стрічок мого коду, а тут лише декілька стрічок. Реальний проєкт, подібний до цього тесту, як по тематиці, так і по технології - я описав ось тут - Unit-тести для ASP.NET MVC.
Моя зацікавленість у цієї фірмі полягала у тому, що ці фірма розташована у тому же невеличкому місти Бургасі, де я зараз живу. І працюють вони на .NET (що рідко для Бургаса). Тому я прочитав завдання і запропонував работодавцю виконати цей тестове завдання на місці, безпосередньо після співбесіди. Я сказав, що звичайно у якості ORM я використовую Linq-to-SQL, могу і на EntityFramework. Відповідь була - це не так важливо, можете написати цій тест хоч на ADO.NET - але на MVC, не на ASP.NET Classic! Контролів DevExpress у мене вдома немає, бо це платна прога. Колись я ії використовував у якихось проєктах, здалося це нецікавим. Добре, відповів я, тоді я зроблю вам на Linq-to-SQL, MVC і найшвидши для роботи і програмування засобом. І я зробив цей тест, вислав його работодавцю, рекрутер підтвердив, що фірма отримала результат тесту, але після цього фірма з зв'язку зникла. Зробили вигляд, нібито теста мені не доручали виконати, ніякого листа щодо роботи від них я не отримав. Це кидок у чистому вигляді. Заставили мене день попрацювати безкоштовно. Тому викладаю у інтернеті і цей опис і їх завдання і його рішення.
По-перше я зробив базу. Зрозуміло, що у реальному світі базу би можливо зробити ретельніше, ну наприклад додати дати реєстрації усіх об'єктів, бани юзеров тощо. Розвивати проєкт можливо скільки завгодно. Але для тестів я я зробив саме так.
1: USE [TheHouse]2: GO3:
4: SET ANSI_NULLS ON5: GO6: SET QUOTED_IDENTIFIER ON7: GO8: CREATE TABLE [dbo].[User](9: [id] [uniqueidentifier] NOT NULL,10: [Login] [nvarchar](50) NOT NULL,11: [Pass] [nvarchar](50) NOT NULL,12: [IsAdmin] [int] NOT NULL,13: CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED14: (
15: [id] ASC16: )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]17: ) ON [PRIMARY]18: GO19:
20: SET ANSI_NULLS ON21: GO22: SET QUOTED_IDENTIFIER ON23: GO24: CREATE TABLE [dbo].[AutoBrand](25: [i] [int] IDENTITY(1,1) NOT NULL,26: [AutoBrandName] [nvarchar](50) NOT NULL,27: CONSTRAINT [PK_AutoBrand] PRIMARY KEY CLUSTERED28: (
29: [i] ASC30: )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]31: ) ON [PRIMARY]32: GO33:
34: SET ANSI_NULLS ON35: GO36: SET QUOTED_IDENTIFIER ON37: GO38: CREATE TABLE [dbo].[ProductCategory](39: [i] [int] IDENTITY(1,1) NOT NULL,40: [CategoryName] [nvarchar](50) NOT NULL,41: CONSTRAINT [PK_ProductCategory] PRIMARY KEY CLUSTERED42: (
43: [i] ASC44: )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]45: ) ON [PRIMARY]46: GO47:
48: SET ANSI_NULLS ON49: GO50: SET QUOTED_IDENTIFIER ON51: GO52: CREATE TABLE [dbo].[Country](53: [i] [int] IDENTITY(1,1) NOT NULL,54: [CountryName] [nvarchar](50) NOT NULL,55: CONSTRAINT [PK_Country] PRIMARY KEY CLUSTERED56: (
57: [i] ASC58: )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]59: ) ON [PRIMARY]60: GO61:
62: SET ANSI_NULLS ON63: GO64: SET QUOTED_IDENTIFIER ON65: GO66: CREATE TABLE [dbo].[AutoModel](67: [id] [uniqueidentifier] NOT NULL,68: [ModelName] [nvarchar](50) NOT NULL,69: [FromYear] [int] NOT NULL,70: [ToYear] [int] NOT NULL,71: [ToAutoBrand] [int] NOT NULL,72: CONSTRAINT [PK_AutoModel] PRIMARY KEY CLUSTERED73: (
74: [id] ASC75: )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]76: ) ON [PRIMARY]77: GO78:
79: SET ANSI_NULLS ON80: GO81: SET QUOTED_IDENTIFIER ON82: GO83: CREATE TABLE [dbo].[Supplier](84: [id] [uniqueidentifier] NOT NULL,85: [SupplierName] [nvarchar](250) NOT NULL,86: [ToCountry] [int] NOT NULL,87: [SupplierAddress] [nvarchar](250) NOT NULL,88: [SupplierContact] [nvarchar](250) NOT NULL,89: CONSTRAINT [PK_Supplier] PRIMARY KEY CLUSTERED90: (
91: [id] ASC92: )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]93: ) ON [PRIMARY]94: GO95:
96: SET ANSI_NULLS ON97: GO98: SET QUOTED_IDENTIFIER ON99: GO100: create View [dbo].[AllAutoModel] as101: select * from dbo.AutoModel join dbo.AutoBrand102: on dbo.AutoModel.ToAutoBrand=dbo.AutoBrand.i103: GO104:
105: SET ANSI_NULLS ON106: GO107: SET QUOTED_IDENTIFIER ON108: GO109: CREATE TABLE [dbo].[Product](110: [id] [uniqueidentifier] NOT NULL,111: [Code] [nvarchar](50) NOT NULL,112: [Name] [nvarchar](250) NOT NULL,113: [ToCategory] [int] NOT NULL,114: [PriceIN] [money] NOT NULL,115: [PriceOUT] [money] NOT NULL,116: [ToAutoModel] [uniqueidentifier] NOT NULL,117: [ToSupplier] [uniqueidentifier] NOT NULL,118: CONSTRAINT [PK_Product] PRIMARY KEY CLUSTERED119: (
120: [id] ASC121: )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]122: ) ON [PRIMARY]123: GO124:
125: SET ANSI_NULLS ON126: GO127: SET QUOTED_IDENTIFIER ON128: GO129: create View [dbo].[AllSupplier] as130: select * from dbo.Supplier join dbo.Country131: on dbo.Supplier.ToCountry=dbo.Country.i132: GO133:
134: SET ANSI_NULLS ON135: GO136: SET QUOTED_IDENTIFIER ON137: GO138: create View [dbo].[AllProduct] as139: select dbo.Product.*,140: dbo.ProductCategory.CategoryName,
141: dbo.AutoModel.id as AutoModel_id,142: dbo.AutoModel.ModelName,
143: dbo.AutoModel.FromYear,
144: dbo.AutoModel.ToYear,
145: dbo.AutoModel.ToAutoBrand,
146: dbo.AutoBrand.AutoBrandName,
147: dbo.Supplier.id as Supplier_id,148: dbo.Supplier.SupplierAddress,
149: dbo.Supplier.SupplierContact,
150: dbo.Supplier.SupplierName,
151: dbo.Supplier.ToCountry,
152: dbo.Country.CountryName
153: from dbo.Product154: join dbo.ProductCategory on dbo.Product.ToCategory=dbo.ProductCategory.i155: join dbo.AutoModel on dbo.AutoModel.id=dbo.Product.ToAutoModel156: join dbo.AutoBrand on dbo.AutoModel.ToAutoBrand=dbo.AutoBrand.i157: join dbo.Supplier on dbo.Supplier.id=dbo.Product.ToSupplier158: join dbo.Country on dbo.Country.i=dbo.Supplier.ToCountry159: GO160:
161: ALTER TABLE [dbo].[AutoModel] WITH CHECK ADD CONSTRAINT [FK_AutoModel_AutoBrand] FOREIGN KEY([ToAutoBrand])162: REFERENCES [dbo].[AutoBrand] ([i])163: GO164: ALTER TABLE [dbo].[AutoModel] CHECK CONSTRAINT [FK_AutoModel_AutoBrand]165: GO166:
167: ALTER TABLE [dbo].[Product] WITH CHECK ADD CONSTRAINT [FK_Product_AutoModel] FOREIGN KEY([ToAutoModel])168: REFERENCES [dbo].[AutoModel] ([id])169: GO170: ALTER TABLE [dbo].[Product] CHECK CONSTRAINT [FK_Product_AutoModel]171: GO172:
173: ALTER TABLE [dbo].[Product] WITH CHECK ADD CONSTRAINT [FK_Product_ProductCategory] FOREIGN KEY([ToCategory])174: REFERENCES [dbo].[ProductCategory] ([i])175: GO176: ALTER TABLE [dbo].[Product] CHECK CONSTRAINT [FK_Product_ProductCategory]177: GO178:
179: ALTER TABLE [dbo].[Product] WITH CHECK ADD CONSTRAINT [FK_Product_Supplier] FOREIGN KEY([ToSupplier])180: REFERENCES [dbo].[Supplier] ([id])181: GO182: ALTER TABLE [dbo].[Product] CHECK CONSTRAINT [FK_Product_Supplier]183: GO184:
185: ALTER TABLE [dbo].[Supplier] WITH CHECK ADD CONSTRAINT [FK_Supplier_Country] FOREIGN KEY([ToCountry])186: REFERENCES [dbo].[Country] ([i])187: GO188: ALTER TABLE [dbo].[Supplier] CHECK CONSTRAINT [FK_Supplier_Country]189: GO
Цей тест я зробив на своїх улюблених технологіях MVC 4 у VS2010 у сінтаксісі RAZOR, тому я використував Linq-To-SQL, бо він і працює швидше, ніж EntityFramework і вже має в собі код репозіторі-патерна для роботи з даними.
1: <?xml version="1.0" encoding="utf-8"?>2: <!--3: For more information on how to configure your ASP.NET application, please visit4: http://go.microsoft.com/fwlink/?LinkId=1694335: -->6:
7: <configuration>8: <appSettings>9: <!-- Этот адрес нужен для работы на разных машинах -->10: <add key="LoginDomain" value="localhost"/>11: <!-- Этот адрес нужен для активации логина и сброса пароля -->12: <add key="HostingURL" value="localhost:59748"/>13: <!-- -->14: <add key="webpages:Version" value="2.0.0.0" />15: <add key="webpages:Enabled" value="false" />16: <add key="PreserveLoginUrl" value="true" />17: <add key="ClientValidationEnabled" value="true" />18: <add key="UnobtrusiveJavaScriptEnabled" value="true" />19: </appSettings>20:
21: <connectionStrings>22: <add name="TheHouseConnectionString" connectionString="Data Source=XXXXXXXXX;Initial Catalog=TheHouse;User ID=YYYYYYYYYYYY;Password=ZZZZZZZZZZZZZZZ" providerName="System.Data.SqlClient" />23: </connectionStrings>24:
25: <system.web>26: <customErrors mode="Off" />27: <compilation debug="true" targetFramework="4.0" />28: <authentication mode="Forms">29: <forms loginUrl="~/Login/Index" timeout="2880" />30: </authentication>31: <pages>32: <namespaces>33: <add namespace="System.Web.Helpers" />34: <add namespace="System.Web.Mvc" />35: <add namespace="System.Web.Mvc.Ajax" />36: <add namespace="System.Web.Mvc.Html" />37: <add namespace="System.Web.Routing" />38: <add namespace="System.Web.WebPages" />39: <add namespace="System.Web.Optimization"/>40: </namespaces>41: </pages>42: </system.web>
Далі я додав це два класа, бо було зрозуміло, що потрібно буде відображати дані у комбобоксах та для админкі буде потрібна аутентифікація. Зрозуміло, що в реальному світі все робиться інакше, тобто справочники вичитуються з бази один раз за весь період рециклінгу сайту, тобто раз на добу, а не при кожному реквесті. Але ж це тест, тобто просто бесплатна робота, мастер-класс для ідіотів, безкоштовне марнотратство часу. Тому тільки так. Краще тільки за гроші.
Ось код ціх класів.
1: Public Class PartRef2:
3: Public Property CatList As System.Collections.Generic.List(Of Mvc.SelectListItem)4: Public Property ModelList As System.Collections.Generic.List(Of Mvc.SelectListItem)5: Public Property SupList As System.Collections.Generic.List(Of Mvc.SelectListItem)6:
7: Public Sub New()8: Dim db1 As New TheHouseDBDataContext9: SupList = New System.Collections.Generic.List(Of Mvc.SelectListItem)10: CatList = New System.Collections.Generic.List(Of Mvc.SelectListItem)11: ModelList = New System.Collections.Generic.List(Of Mvc.SelectListItem)12: Dim Mod1 = (From X In db1.AllAutoModels Select X).ToList13: Dim Sup1 = (From X In db1.AllSuppliers Select X).ToList14: Dim Cat1 = (From X In db1.ProductCategories).ToList15: For Each One In Mod116: ModelList.Add(New SelectListItem With {.Value = One.id.ToString, .Text = One.ModelName, .Selected = False})17: Next18: For Each One In Sup119: SupList.Add(New SelectListItem With {.Value = One.id.ToString, .Text = One.SupplierName, .Selected = False})20: Next21: For Each One In Cat122: CatList.Add(New SelectListItem With {.Value = One.i, .Text = One.CategoryName, .Selected = False})23: Next24: End Sub25:
26: End Class
1: Public Class AU2: Shared Sub SetAU(Login As String, Remember As String)3: If Remember.Contains("true") Then4: Dim ticket As New FormsAuthenticationTicket(1, Login, Now, Now.AddYears(1), True, ", FormsAuthentication.FormsCookiePath)5: Dim EncryptTicket As String = FormsAuthentication.Encrypt(ticket)6: Dim AUCook As HttpCookie = New HttpCookie(FormsAuthentication.FormsCookieName, EncryptTicket)7: AUCook.Domain = System.Configuration.ConfigurationManager.AppSettings("LoginDomain")8: AUCook.Expires = Now.AddYears(1)
9: HttpContext.Current.Response.Cookies.Add(AUCook)
10: Else11: FormsAuthentication.SetAuthCookie(Login, True)12: End If13: End Sub14:
15: Shared Function GetCurrentUser() As Global.TheHouseVB.User16: Dim db1 As New TheHouseDBDataContext17: Dim User1 = (From X In db1.Users Select X Where X.Login = HttpContext.Current.User.Identity.Name).ToList18: If User1.Count > 0 Then19: Return User1(0)20: Else21: Return Nothing22: End If23: End Function24:
25: Shared Sub DelAU()26: FormsAuthentication.SignOut()
27: End Sub28:
29: End Class
Далі я послідовно зробив усі чотири контролера, код їх занудний, коментувати тут нічого. Якщо б функції цих контролерів були реальні, зрозуміло що код потрібно було в виносити в окремий рівень, робити класи для моделей і так далі - але тут по-перше весь код по три стрічки, подруге і це забагато для безкоштовної роботи.
Мабуть едина особливість цього коду - третья стрічка наступного коду, тобто я унаслідовав Admin-контролер від User-контролеру.
1: Namespace TheHouseVB2: Public Class AdminController3: Inherits UserController4:
5: Function Users() As ActionResult6: Return View((From X In db1.Users Select X).ToList)7: End Function8:
9: Function Parts() As ActionResult10: ViewData("IsAdmin") = CurrentUser.IsAdmin11: Return View((From X In db1.AllProducts Select X).ToList)12: End Function13:
14: Function Suppliers() As ActionResult15: Return View((From X In db1.AllSuppliers Select X).ToList)16: End Function17:
18: Function AutoModels() As ActionResult19: Return View((From X In db1.AllAutoModels Select X).ToList)20: End Function21:
22: Function Categories() As ActionResult23: Return View((From X In db1.ProductCategories Select X).ToList)24: End Function25:
26: Function AddNewPart() As ActionResult27: Return View(New Global.TheHouseVB.PartRef)28: End Function29:
30: <HttpPost()>31: Function AddNewPart(Prm As FormCollection) As ActionResult32: Try33: db1.Products.InsertOnSubmit(New Product With {34: .id = Guid.NewGuid,
35: .Code = Prm("code"),36: .Name = Prm("name"),37: .PriceIN = Prm("in"),38: .PriceOUT = Prm("out"),39: .ToCategory = Prm("cat"),40: .ToSupplier = Guid.Parse(Prm("sup")),41: .ToAutoModel = Guid.Parse(Prm("mod"))42: })
43: db1.SubmitChanges()
44: Return RedirectToAction("Parts")45: Catch ex As Exception46: ViewData("Err1") = ex.Message47: Return View()48: End Try49: End Function50:
51: Function AddNewUser() As ActionResult52: Return View()53: End Function54:
55: <HttpPost()>56: Function AddNewUser(Prm As FormCollection) As ActionResult57: Try58: db1.Users.InsertOnSubmit(New User With {.id = Guid.NewGuid, .IsAdmin = 0, .Login = Prm("email"), .Pass = ("pass")})59: db1.SubmitChanges()
60: Return RedirectToAction("Users")61: Catch ex As Exception62: ViewData("Err1") = ex.Message63: Return View()64: End Try65: End Function66:
67: Function AddNewCategory() As ActionResult68: Return View()69: End Function70:
71: <HttpPost()>72: Function AddNewCategory(Prm As FormCollection) As ActionResult73: Try74: db1.ProductCategories.InsertOnSubmit(New ProductCategory With {.CategoryName = Prm("cat")})75: db1.SubmitChanges()
76: Return RedirectToAction("Categories")77: Catch ex As Exception78: ViewData("Err1") = ex.Message79: Return View()80: End Try81: End Function82:
83: Function AddNewModel() As ActionResult84: Dim BrandsList As New System.Collections.Generic.List(Of Mvc.SelectListItem)85: Dim Brands = (From X In db1.AutoBrands Select X).ToList86: For Each One In Brands87: BrandsList.Add(New SelectListItem With {.Value = One.i, .Text = One.AutoBrandName, .Selected = False})88: Next89: Return View(BrandsList)90: End Function91:
92: <HttpPost()>93: Function AddNewModel(Prm As FormCollection) As ActionResult94: Try95: db1.AutoModels.InsertOnSubmit(New AutoModel With {.id = Guid.NewGuid, .FromYear = Prm("from"), .ToYear = Prm("to"), .ModelName = Prm("name"), .ToAutoBrand = Prm("brand")})96: db1.SubmitChanges()
97: Return RedirectToAction("AutoModels")98: Catch ex As Exception99: ViewData("Err1") = ex.Message100: Return View()101: End Try102: End Function103:
104: Function AddNewSupplier() As ActionResult105: Dim CountryList As New System.Collections.Generic.List(Of Mvc.SelectListItem)106: Dim Country = (From X In db1.Countries Select X).ToList107: For Each One In Country108: CountryList.Add(New SelectListItem With {.Value = One.i, .Text = One.CountryName, .Selected = False})109: Next110: Return View(CountryList)111: End Function112:
113: <HttpPost()>114: Function AddNewSupplier(Prm As FormCollection) As ActionResult115: Try116: db1.Suppliers.InsertOnSubmit(New Supplier With {.ToCountry = Prm("country"), .SupplierAddress = Prm("addr"), .SupplierContact = Prm("cont"), .SupplierName = Prm("name")})117: db1.SubmitChanges()
118: Return RedirectToAction("Suppliers")119: Catch ex As Exception120: ViewData("Err1") = ex.Message121: Return View()122: End Try123: End Function124:
125: Function DelUser(ID As String) As ActionResult126: Try127: Dim User1 = (From X In db1.Users Select X Where X.id.ToString = ID).ToList128: If User1.Count > 0 Then129: db1.Users.DeleteOnSubmit(User1(0))
130: db1.SubmitChanges()
131: End If132: Return RedirectToAction("Users")133: Catch ex As Exception134: ViewData("Err1") = ex.Message135: Return View("Users", (From X In db1.Users Select X).ToList)136: End Try137:
138: End Function139:
140: Function DelPart(ID As String) As ActionResult141: Try142: Dim Prod1 = (From X In db1.Products Select X Where X.id.ToString = ID).ToList143: If Prod1.Count > 0 Then144: db1.Products.DeleteOnSubmit(Prod1(0))
145: db1.SubmitChanges()
146: End If147: Return RedirectToAction("Parts")148: Catch ex As Exception149: ViewData("Err1") = ex.Message150: ViewData("IsAdmin") = CurrentUser.IsAdmin151: Return View("Parts", (From X In db1.AllProducts Select X).ToList)152: End Try153: End Function154:
155: Function DelSuppl(ID As String) As ActionResult156: Try157: Dim Sup1 = (From X In db1.Suppliers Select X Where X.id.ToString = ID).ToList158: If Sup1.Count > 0 Then159: db1.Suppliers.DeleteOnSubmit(Sup1(0))
160: db1.SubmitChanges()
161: End If162: Return RedirectToAction("Suppliers")163: Catch ex As Exception164: ViewData("Err1") = ex.Message165: Return View("Suppliers", (From X In db1.AllSuppliers Select X).ToList)166: End Try167: End Function168:
169: Function DelModel(ID As String) As ActionResult170: Try171: Dim Mod1 = (From X In db1.AutoModels Select X Where X.id.ToString = ID).ToList172: If Mod1.Count > 0 Then173: db1.AutoModels.DeleteOnSubmit(Mod1(0))
174: db1.SubmitChanges()
175: End If176: Return RedirectToAction("AutoModels")177: Catch ex As Exception178: ViewData("Err1") = ex.Message179: Return View("AutoModels", (From X In db1.AllAutoModels Select X).ToList)180: End Try181: End Function182:
183: Function DelCat(ID As String) As ActionResult184: Try185: Dim Cat1 = (From X In db1.ProductCategories Select X Where X.i.ToString = ID).ToList186: If Cat1.Count > 0 Then187: db1.ProductCategories.DeleteOnSubmit(Cat1(0))
188: db1.SubmitChanges()
189: End If190: Return RedirectToAction("Categories")191: Catch ex As Exception192: ViewData("Err1") = ex.Message193: Return View("Categories", (From X In db1.ProductCategories Select X).ToList)194: End Try195: End Function196:
197:
198: End Class199: End Namespace
1: Namespace TheHouseVB2: Public Class HomeController3: Inherits System.Web.Mvc.Controller4:
5: Public CurrentUser As Global.TheHouseVB.User6:
7: Protected Overrides Sub OnActionExecuting(ctx As System.Web.Mvc.ActionExecutingContext)8: MyBase.OnActionExecuting(ctx)9: CurrentUser = AU.GetCurrentUser
10: If CurrentUser IsNot Nothing Then11: If CurrentUser.IsAdmin = 1 Then12: ctx.HttpContext.Response.Redirect("/Admin/Index")13: Else14: ctx.HttpContext.Response.Redirect("/Cab/Index")15: End If16: End If17: End Sub18:
19: Function Index() As ActionResult20: Dim db1 As New TheHouseDBDataContext21: Dim Data = (From X In db1.AllProducts Select X).ToList22: ViewData("IsAdmin") = 023: Return View(Data)24: End Function25:
26: Function OnePart(ID As String) As ActionResult27: Dim db1 As New TheHouseDBDataContext28: Dim Data = (From X In db1.AllProducts Select X Where X.id.ToString = ID).ToList29: ViewData("IsAdmin") = 030: Return View(Data(0))31: End Function32:
33: Function Target() As ActionResult34: Return View()35: End Function36:
37: <HttpPost()> _38: Function Search(Prm As FormCollection)39: ViewData("IsAdmin") = 040: Dim db1 As New TheHouseDBDataContext41: Dim Data = (From X In db1.AllProducts Select X Where X.Code Like Prm("search")).ToList42: Return View("Index", Data)43: End Function44:
45: Function GetProjectCode(id As String) As ActionResult46: If id = "VB" Then47: Return File("~/TheHouseVB1.zip", "application/octet-stream", "TheHouseVB1.zip")48: ElseIf id = "CS" Then49: Return File("~/TheHouseCS1.zip", "application/octet-stream", "TheHouseCS1.zip")50: ElseIf id = "SQL" Then51: Return File("~/TheHouseSQL.zip", "application/octet-stream", "TheHouseSQL.zip")52: End If53: End Function54:
55:
56: End Class57: End Namespace
1: Namespace TheHouseVB2: Public Class LoginController3: Inherits System.Web.Mvc.Controller4:
5: Function Index() As ActionResult6: Return View()7: End Function8:
9: <HttpPost()>10: Function Index(Prm As FormCollection) As ActionResult11: Dim db1 As New TheHouseDBDataContext12: Dim AdmUser = (From X In db1.Users Select X Where X.Login = Prm("name") And X.Pass = Prm("pass") And X.IsAdmin = 1).ToList13: If AdmUser.Count > 0 Then14: AU.SetAU(Prm("name"), Prm("remember"))15: Return RedirectToAction("Index", "Admin")16: End If17: Dim NoAdmUser = (From X In db1.Users Select X Where X.Login = Prm("name") And X.Pass = Prm("pass") And X.IsAdmin = 0).ToList18: If NoAdmUser.Count > 0 Then19: AU.SetAU(Prm("name"), Prm("remember"))20: Return RedirectToAction("Index", "User")21: End If22: Return View()23: End Function24:
25: Function Register() As ActionResult26: Return View()27: End Function28:
29: <HttpPost()>30: Function Register(Prm As FormCollection) As ActionResult31: Dim db1 As New TheHouseDBDataContext32: db1.Users.InsertOnSubmit(New Global.TheHouseVB.User With {.id = Guid.NewGuid, .Login = Prm("email"), .Pass = Prm("pass")})33: db1.SubmitChanges()
34: AU.SetAU(Prm("email"), Prm("pass"))35: Return RedirectToAction("Index", "User")36: End Function37: End Class38: End Namespace
1: Namespace TheHouseVB2: Public Class UserController3: Inherits System.Web.Mvc.Controller4:
5: Public CurrentUser As Global.TheHouseVB.User6: Public db1 As TheHouseDBDataContext7:
8: Protected Overrides Sub OnActionExecuting(ctx As System.Web.Mvc.ActionExecutingContext)9: MyBase.OnActionExecuting(ctx)10: db1 = New TheHouseDBDataContext11: CurrentUser = AU.GetCurrentUser
12: If CurrentUser Is Nothing Then ctx.HttpContext.Response.Redirect("/Home/Index")13: End Sub14:
15: Function Index() As ActionResult16: ViewData("UserName") = CurrentUser.Login17: ViewData("IsAdmin") = CurrentUser.IsAdmin18: Return View((From X In db1.AllProducts Select X).ToList)19: End Function20:
21: Function OnePart(ID As String) As ActionResult22: Dim db1 As New TheHouseDBDataContext23: Dim Data = (From X In db1.AllProducts Select X Where X.id.ToString = ID).ToList24: ViewData("IsAdmin") = CurrentUser.IsAdmin25: Return View(Data(0))26: End Function27:
28: Function LogOff() As ActionResult29: AU.DelAU()
30: Return RedirectToAction("Index", "Home")31: End Function32:
33: <HttpPost()> _34: Function Search(Prm As FormCollection)35: ViewData("IsAdmin") = 036: Dim db1 As New TheHouseDBDataContext37: Dim Data = (From X In db1.AllProducts Select X Where X.Code Like Prm("search")).ToList38: Return View("Index", Data)39: End Function40:
41:
42: Function AddPart(ID As String) As ActionResult43: 'no code to processing user basket - it's only a demo site44: Return RedirectToAction("Index")45: End Function46:
47:
48: End Class49: End Namespace
Зрозуміло, що у реальних проєктах таких простих OnActionExecuting теж не може буте, це перший кандидат на тисячі та десятки тисяч стрічок коду.
Робота сайта починається з ініціалізації, чомусь у мене у MVC 4 на працює оптимізація JavaScript, але це не важливо, у реальних проєктах таких простих конфігів все одно не буває. Це друге місце будь-якого проєкту, яке починає розбухати до тисячів і десятків тисяч стрічок коду.
Взагалі тут і пакувати нічого, тільки CSS, я навіть jQuery додав про запас, я ним не користувався у цьому проєкті.
Ну і код усіх форм публікувати немає бажання, повністю завантажити проєкт ви можете з самого тестового сайту вони зроблені однаково. Я покажу їх на скринах.
Зрозуміло, що в реальних проєктах так не робиться. Щонайменше використовується яка-небудь CMS. Я маю декілька своїх власних CMS для подібних MVC-проєктів. Ось тут описана моя улюблена - Моя CMS для ASP.NET MVC..
2. Тест для компанії bobs.bg (C#, MVC 5, AJAX, EntityFramework)
Цей тест був значно важкий для мене, бо по-перше його потрібно будо виконати на шарпі, а по-друге у дуже обмежений час. І сама технологія булу значно важкої, порівняно з попереднім тестом - C# + AJAX + EntityFramework. До того го ж, моя студія не прочитала базу, яку вони мені дали. Із-за цієї бази я встиг зробити цей тест, але з невеличкою затримкою, десь на годину. Я пояснив їм все це з базою, вони зрозуміли, і відповіли, что тест зарахований як пройдений. Але після відповіді зникли так же точно, як і попередня фірма. Ось це завдання.
Тобто до мене у цієї тестової задачці хтось колупався, щось почав, я нібито продовжую з тієї точки, на який зупинився попередник, який намагався отримати роботу у цієї фірмі. І наступний програміст, як я розумію, почне з того місця, з якого я закінчив. Ну щось, це розумно - так можно величезний проєкт довести до кінця, при чому з великою якістю та у короткий строк!
Але справа в тому, що всі програмисти працюють трошки у різному середовищу, мені значно простіше було б почати взагалі проєкт спочатку, але у базі були дані, і тому я витратив важливу годину, поки зумів прочитати базу, який наковиряв попередній програміст.
Взагалі нова студія працює з базами якось незрозуміло, наприклад щоб відкрити базу у Server Explorer спочатку потрібно видалити LDF-файл. Але врешті-решт мені вдалося прочитати базу. На скринах нище вже моя база, знімати скрини під час тесту у мене не було часу, я був злий, бо розумів що витрачаю час марно.
Як бачите, глобально це WEB-проект на NET 4.5
1: <?xml version="1.0" encoding="utf-8"?>2: <!--3: For more information on how to configure your ASP.NET application, please visit4: http://go.microsoft.com/fwlink/?LinkId=1523685: -->6: <configuration>7: <configSections>8: <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->9: <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />10: </configSections>11: <connectionStrings>12: <add name="DevTestDBEntities" connectionString="metadata=res://*/Models.DevTest.csdl|res://*/Models.DevTest.ssdl|res://*/Models.DevTest.msl;provider=System.Data.SqlClient;provider connection string="data source=(LocalDB)\v11.0;attachdbfilename=|DataDirectory|\DevTestDB.mdf;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" />13: </connectionStrings>14: <appSettings>15: <add key="webpages:Version" value="2.0.0.0" />16: <add key="webpages:Enabled" value="false" />17: <add key="PreserveLoginUrl" value="true" />18: <add key="ClientValidationEnabled" value="true" />19: <add key="UnobtrusiveJavaScriptEnabled" value="true" />20: </appSettings>21: <system.web>22: <httpRuntime targetFramework="4.5" />23: <compilation debug="true" targetFramework="4.5">24: <assemblies>25: <add assembly="System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />26: </assemblies>27: </compilation>28: <authentication mode="Forms">29: <forms loginUrl="~/Account/Login" timeout="2880" />30: </authentication>31: <pages>32: <namespaces>33: <add namespace="System.Web.Helpers" />34: <add namespace="System.Web.Mvc" />35: <add namespace="System.Web.Mvc.Ajax" />36: <add namespace="System.Web.Mvc.Html" />37: <add namespace="System.Web.Optimization" />38: <add namespace="System.Web.Routing" />39: <add namespace="System.Web.WebPages" />40: </namespaces>41: </pages>42: <profile defaultProvider="DefaultProfileProvider">43: <providers>44: <add name="DefaultProfileProvider" type="System.Web.Providers.DefaultProfileProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" applicationName="/" />45: </providers>46: </profile>47: <membership defaultProvider="DefaultMembershipProvider">48: <providers>49: <add name="DefaultMembershipProvider" type="System.Web.Providers.DefaultMembershipProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" />50: </providers>51: </membership>52: <roleManager defaultProvider="DefaultRoleProvider">53: <providers>54: <add name="DefaultRoleProvider" type="System.Web.Providers.DefaultRoleProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" applicationName="/" />55: </providers>56: </roleManager>57: <!--58: If you are deploying to a cloud environment that has multiple web server instances,59: you should change session state mode from "InProc" to "Custom". In addition,60: change the connection string named "DefaultConnection" to connect to an instance61: of SQL Server (including SQL Azure and SQL Compact) instead of to SQL Server Express.62: -->63: <sessionState mode="InProc" customProvider="DefaultSessionProvider">64: <providers>65: <add name="DefaultSessionProvider" type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" />66: </providers>67: </sessionState>68: </system.web>69: <system.webServer>70: <validation validateIntegratedModeConfiguration="false" />71: <handlers>72: <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />73: <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />74: <remove name="ExtensionlessUrlHandler-Integrated-4.0" />75: <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />76: <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />77: <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />78: </handlers>79: </system.webServer>80: <runtime>81: <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">82: <dependentAssembly>83: <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" />84: <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="2.0.0.0" />85: </dependentAssembly>86: <dependentAssembly>87: <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />88: <bindingRedirect oldVersion="1.0.0.0-4.0.0.0" newVersion="4.0.0.0" />89: </dependentAssembly>90: <dependentAssembly>91: <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" />92: <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="2.0.0.0" />93: </dependentAssembly>94: <dependentAssembly>95: <assemblyIdentity name="EntityFramework" publicKeyToken="b77a5c561934e089" />96: <bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />97: </dependentAssembly>98: <dependentAssembly>99: <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" />100: <bindingRedirect oldVersion="0.0.0.0-1.3.0.0" newVersion="1.3.0.0" />101: </dependentAssembly>102: </assemblyBinding>103: </runtime>104: <entityFramework>105: <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">106: <parameters>107: <parameter value="v12.0" />108: </parameters>109: </defaultConnectionFactory>110: </entityFramework>111: </configuration>
У цьому середовище BundleConfig у мене працює нормально, тому старт проекту звичайний.
Зрозуміло, що в першу чергу у цьому проєкті потрібні моделі, бо саме їх потрібно буде вводити з форм, обробляти та додавати у базу.
Я зробив модель по-модному - з атрибутами, якими вміє користуватися jQuery.Validate.
1: using System;2: using System.Collections.Generic;3: using System.Linq;4: using System.Web;5: using System.Web.Mvc;6: using System.ComponentModel;7: using System.ComponentModel.DataAnnotations;8:
9: namespace UATP.DevTest.ViewModel10: {
11: public class PersonAddressViewModel12: {
13: public string Id { get; set; }14:
15:
16: [Required]
17: [StringLength(20)]
18: [RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "Use letters only please")]19: public string FirstName { get; set; }20: [Required]
21: [StringLength(20)]
22: [RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "Use letters only please")]23: public string LastName { get; set; }24: [Required]
25: [StringLength(20)]
26: [RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "Use letters only please")]27: public string AddressLine { get; set; }28: [Required]
29: [StringLength(10)]
30: public string City { get; set; }31: [Required]
32: [StringLength(5)]
33: [RegularExpression(@"^\d+", ErrorMessage = "Use number only please")]34: public string PostalCode { get; set; }35: public DateTime ModifiedDate { get; set; }36: }
37: }
Далі, форма за формою я зробив всі форми, ну перші чотири форми тут взагалі нецікаві.
1: @model UATP.DevTest.ViewModel.PersonAddressViewModel2:
3: <h2>Create</h2>4:
5: @using (Html.BeginForm()) {
6: @Html.AntiForgeryToken()
7: @Html.ValidationSummary(true)
8:
9: <fieldset>10: <div class="editor-label">11: @Html.LabelFor(model => model.FirstName)12: </div>13: <div class="editor-field">14: @Html.EditorFor(model => model.FirstName)15: @Html.ValidationMessageFor(model => model.FirstName)16: </div>17:
18: <div class="editor-label">19: @Html.LabelFor(model => model.LastName)20: </div>21: <div class="editor-field">22: @Html.EditorFor(model => model.LastName)23: @Html.ValidationMessageFor(model => model.LastName)24: </div>25:
26: <div class="editor-label">27: @Html.LabelFor(model => model.AddressLine)28: </div>29: <div class="editor-field">30: @Html.EditorFor(model => model.AddressLine)31: @Html.ValidationMessageFor(model => model.AddressLine)32: </div>33:
34: <div class="editor-label">35: @Html.LabelFor(model => model.City)36: </div>37: <div class="editor-field">38: @Html.EditorFor(model => model.City)39: @Html.ValidationMessageFor(model => model.City)40: </div>41:
42: <div class="editor-label">43: @Html.LabelFor(model => model.PostalCode)44: </div>45: <div class="editor-field">46: @Html.EditorFor(model => model.PostalCode)47: @Html.ValidationMessageFor(model => model.PostalCode)48: </div>49:
50: <p>51: <input type="submit" value="Create" />52: </p>53: </fieldset>54: }
55:
56: <div>57: @Html.ActionLink("Back to List", "Index")
58: </div>59:
60: @section Scripts {61: @Scripts.Render("~/bundles/jqueryval")
62: }
Більш-менш цікаві тут форми роботи з AJAX і скрипти. Я міг би об'єднати ці дві форми в одну, як у тести вище, но цього разу зробив дві окремі форми.
А ось і сама перлинка цього завдання - три невелички AJAX-запроса.
Зверніть увагу, що всі три запроса зроблені за допомогою jquery.unobtrusive, але перші два зроблені за допомогою існуючого хелпера, а останній - ручками. Мабуть у цьому полягала хитрість тестерів? Незрозуміло. Чи може було потрібно зробити модний хелпер з третього AJAX? Незрозуміло. Якщо я мав би трошки більше часу, я міг би і свій хелпер додати до проекту. Але так якісно - тільки за гроші.
1: @model IEnumerable<UATP.DevTest.Models.Person>2:
3: Search box<br />4:
5: <fieldset title="Search" style="text-align:right;width:160px;">6: <div class="editor-field">7: FirstName<br />8: @Html.TextBox("FirstName")
9: </div>10: <div class="editor-label">11: LastName<br>12: @Html.TextBox("LastName")
13: </div>14: <div class="editor-field">15: AddressLine<br />16: @Html.TextBox("AddressLine")
17: </div>18: <div class="editor-field">19: City<br />20: @Html.TextBox("City")
21: </div>22: <div class="editor-field">23: PostalCode<br />24: @Html.TextBox("PostalCode")
25: </div>26: <div class="editor-field">27:
28: <a href="#" id="data-search" name="data-search">Search</a>29:
30: </div>31:
32: </fieldset>33:
34:
35: <p>36: @Html.ActionLink("Create New Record", "Create")
37:
38: </p>39:
40: First 3 records
41: <table>42: <tr>43: <th>44: @Html.DisplayNameFor(model => model.FirstName)45: </th>46: <th>47: @Html.DisplayNameFor(model => model.LastName)48: </th>49: <th>50: @Html.DisplayNameFor(model => model.Address.AddressLine)51: </th>52: <th>53: @Html.DisplayNameFor(model => model.Address.City)54: </th>55: <th>56: @Html.DisplayNameFor(model => model.Address.PostalCode)57: </th>58: <th></th>59: </tr>60:
61: @foreach (var item in Model)
62: {
63: <tr>64: <td>65: @Html.DisplayFor(modelItem => item.FirstName)66: </td>67: <td>68: @Html.DisplayFor(modelItem => item.LastName)69: </td>70: <td>71: @Html.DisplayFor(modelItem => item.Address.AddressLine)72: </td>73: <td>74: @Html.DisplayFor(modelItem => item.Address.City)75: </td>76: <td>77: @Html.DisplayFor(modelItem => item.Address.PostalCode)78: </td>79: <td>80: @Html.ActionLink("Edit", "Edit", new { id = item.id }) |
81: @Html.ActionLink("Details", "Detail", new { id = item.id }) |
82: @Html.ActionLink("Delete", "Delete", new { id = item.id })
83: </td>84: </tr>85: }
86:
87: </table>88:
89: <br />90: @Ajax.ActionLink("Show all records with name starts on A-L",
91: "ShowAL",
92: null,
93: new AjaxOptions
94: {
95: HttpMethod = "GET",
96: InsertionMode = InsertionMode.Replace,
97: UpdateTargetId = "content",
98: OnComplete = "refresh1();"
99: }, new { id = "data-al" })
100: <br />101: @Ajax.ActionLink("Show all records with name starts on M-Z",
102: "ShowMZ",
103: null,
104: new AjaxOptions
105: {
106: HttpMethod = "GET",
107: InsertionMode = InsertionMode.Replace,
108: UpdateTargetId = "content",
109: OnComplete = "refresh1();"
110: }, new { id = "data-mz" })
111:
112:
113: <div id="content"></div>114:
115: <script type="text/javascript">116: $(document).ready(function () {117: $("#data-al").click(function (e) {118: e.preventDefault();
119: });
120: $("#data-mz").click(function (e) {121: e.preventDefault();
122: });
123: $("#data-search").click(function (e) {124: e.preventDefault();
125: });
126: });
127: </script>128:
129:
130: <script type="text/javascript">131: $(document).ready(function () {132:
133: function refresh1() {134: };
135:
136: $("#data-search").bind("click", function (e) {137: e.preventDefault();
138: var FirstName = $("#FirstName").val();139: var LastName = $("#LastName").val();140: var AddressLine = $("#AddressLine").val();141: var City = $("#City").val();142: var PostalCode = $("#PostalCode").val();143: $.ajax({
144: url: '/Person/Search',145: type: 'GET',146: data: {
147: "FirstName": FirstName,148: "LastName": LastName,149: "AddressLine": AddressLine,150: "City": City,151: "PostalCode": PostalCode152: },
153: success: function (data) {154: $("#content").replaceWith("<div id='content'>"+data+"</div");155: }
156: });
157: });
158:
159: });
160: </script>161:
162: <br /><br />
Ну і далі залишився тільки контролер, який можна було б зробити тисячами засобів. Наприклад зробити повноцінну BLL, як це робиться у 3-tier application. Но я зробив як мені було легше - тобто коду тут так мало, що його немає сенсу навіть виносити в окрему BLL.
1: using System;2: using System.Collections.Generic;3: using System.Data;4: using System.Linq;5: using System.Text.RegularExpressions;6: using System.Web;7: using System.Web.Mvc;8: using UATP.DevTest.Models;9: using UATP.DevTest.ViewModel;10:
11: namespace UATP.DevTest.Controllers12: {
13: public class PersonController : Controller14: {
15: private DevTestDBEntities _db = new DevTestDBEntities();16:
17: public ActionResult Index()18: {
19: //UPDATE TO RETURN TOP 3 PEOPLE ONLY20: var P = (from Y in _db.People select Y).ToList();21: return View(P.Take(3));22: //return View(P);23: }
24:
25: public ActionResult ShowAL()26: {
27: if (Request.IsAjaxRequest())28: {
29: return PartialView("AjaxView", GetPart(@"^[a-lA-L]"));30: }
31: else return RedirectToAction("Index");32: }
33:
34: public ActionResult ShowMZ()35: {
36: if (Request.IsAjaxRequest())37: {
38: return PartialView("AjaxView", GetPart(@"^[m-zM-Z]"));39: }
40: else return RedirectToAction("Index");41: }
42:
43: private List<Person> GetPart(string str1)44: {
45: var ALregex = new Regex((str1));46: //var P = (from Y in _db.People where ALregex.IsMatch(Y.FirstName) select Y).ToList();47: var P = (from Y in _db.People select Y).ToList();48: var AL_P = new List<Person>();49: foreach (var one in P)50: {
51: if (ALregex.IsMatch(one.FirstName))52: {
53: AL_P.Add(one);
54: }
55: }
56: return AL_P;57: }
58:
59:
60: public ActionResult Search(FormCollection Prm)61: {
62: if (true)63: {
64:
65: string FirstName = Request.QueryString["FirstName"];66: string LastName = Request.QueryString["LastName"];67: string AddressLine = Request.QueryString["AddressLine"];68: string City = Request.QueryString["City"];69: string PostalCode = Request.QueryString["PostalCode"];70:
71: var X = (from P in _db.People72: join A in _db.Addresses on P.AddressID equals A.id73: where (
74: ((FirstName == ") ? false : P.FirstName.Contains(FirstName)) ||75: ((LastName == ") ? false : P.LastName.Contains(FirstName)) ||76: ((AddressLine == ") ? false : A.AddressLine.Contains(AddressLine)) ||77: ((City == ") ? false : A.City.Contains(City)) ||78: ((PostalCode == ") ? false : A.PostalCode.Contains(PostalCode))79: )
80: select P
81: ).ToList();
82:
83: return PartialView("AjaxView2", X);84: }
85: else return RedirectToAction("Index");86: }
87:
88:
89:
90: public ActionResult Details(int id)91: {
92: return View();93: }
94:
95: public ActionResult Create()96: {
97: return View();98: }
99:
100: [HttpPost]
101: public ActionResult Create(PersonAddressViewModel newObj)102: {
103: try104: {
105: return RedirectToAction("Index");106: }
107: catch108: {
109: return View();110: }
111: }
112:
113:
114: public ActionResult Edit(int id)115: {
116: var X = (from P in _db.People117: join A in _db.Addresses on P.AddressID equals A.id118: where P.id == id
119: select new PersonAddressViewModel120: {
121: FirstName = P.FirstName,
122: LastName = P.LastName,
123: AddressLine = A.AddressLine,
124: City = A.City,
125: PostalCode = A.PostalCode,
126: ModifiedDate = A.ModifiedDate.Value,
127: }).ToList();
128: return View(X[0]);129: }
130:
131: [HttpPost]
132: public ActionResult Edit(int id, PersonAddressViewModel newObj)133: {
134: try135: {
136: var P = (from Y in _db.People137: where Y.id == id
138: select Y).ToList();
139: int A_id = P[0].AddressID.Value;140: var A = (from Y in _db.Addresses141: where Y.id == A_id
142: select Y).ToList();
143:
144: if (newObj.AddressLine == A[0].AddressLine &&145: newObj.City == A[0].City &&
146: newObj.PostalCode == A[0].City &&
147: newObj.ModifiedDate == A[0].ModifiedDate)
148: {
149: _db.People.Remove(P[0]);
150:
151: _db.People.Add(new Person152: {
153: FirstName = newObj.FirstName,
154: LastName = newObj.LastName,
155: AddressID = A[0].id
156: });
157:
158: }
159: else160: {
161: var A1 = new Address162: {
163: AddressLine = newObj.AddressLine,
164: City = newObj.City,
165: ModifiedDate = DateTime.Now,
166: PostalCode = newObj.PostalCode
167: };
168: _db.Addresses.Add(A1);
169:
170: _db.SaveChanges();
171: int lastid = A1.id;172:
173: _db.People.Remove(P[0]);
174:
175: _db.People.Add(new Person176: {
177: FirstName = newObj.FirstName,
178: LastName = newObj.LastName,
179: AddressID = lastid
180: });
181: }
182: _db.SaveChanges();
183: return RedirectToAction("Index");184: }
185: catch (Exception ex)186: {
187: return View();188: }
189: }
190:
191: public ActionResult Delete(int id, bool dummy = false)192: {
193: var P = (from Y in _db.People194: where Y.id == id
195: select Y).ToList();
196: _db.People.Remove(P[0]);
197: _db.SaveChanges();
198: return RedirectToAction("Index");199: }
200:
201: [HttpPost]
202: public ActionResult Delete(int id)203: {
204: try205: {
206: return RedirectToAction("Index");207: }
208: catch209: {
210: return View();211: }
212: }
213:
214: public ActionResult Detail(int id)215: {
216: var X = (from P in _db.People217: where P.id == id
218: select P).ToList();
219: return View(X[0]);220: }
221:
222: }
223: }
Тестовий сайт чудово працює, вживую він викладений тут - http://spa2.vb-net.com/. Але, як і у всіх попередніх випадках, після виконання цього теста пропозиції щодо постійної роботи у цієї компанії я не отримав.
3. Тест для компанії Sigma (C#, MVC 5, AJAX, Ninject, EntityFramework, WebAPI2, DTO)
Це найскладний для мене тест, можна сказати, що я його не виконав з повним використанням усіх технологій та бібліотек, які забажав замовник. Тобто тест працює, я його виконав у декількох варіантах, він викладений у інтернеті ось тут - http://spa1.vb-net.com/ - але роботодавцю жодне з моїх рішень не сподобалось. Хм...
Глобально цей тест потребує комбінації патерну, який я описав ось тут - Застосування патерну Dependency Injection за допомогою IoC-контейнера Ninject та SPA-framework. І ще декілька фішок. Але головну частину (якщо це потрібно зробити швидко) я роблю інакше. На тих технологіях, які мені подобаються. Тому далі, я опишу лише самий перший варіант цього тесту, який я зробив швидко, за день, але далі, коли я зрозумів, що все одно я не змогу використати всі бібліотеки, і зробити це якісно та швидко - я облишив цей тест.
Найпростішим засобом цей тест робиться взагалі без WebApi та взагалі без React. Ну і тем більше, без EntityFramework та тестів. Але так (на результат!) працюють у реальному світі. А тестові завдання зроблені саме так, щоб задрочить програміста якимись технологіями, які нікому не потрібні. Ну, наприклад, вище я писав про те, що React у всьому світі використаний на 500 сайтах, це лише трохи більше сайтів, ніж рекламних агенцій, які займаються промоутінгом цєї бібліотеці. Мабуть, навіть, книжок про React випущено більше 500 (кожна величезним тиражом). Але все одно це не привело к використанню цієї бібліотеці массово.
Але, давайте подивимось спочатку, як це можна взагалі зробити найпростішим засобом на jquery.unobtrusive. Я планував спочатку зробити цей сайт якомога простішим засобом, а потім редуціювати його до потрібних у тесті технологій, але по ходу зрозумів, що все одно не встигаю це зробити швидко ... і залишив цю задачку.
Перш за все - ніяке WebApi для вирішення цієї задачки не потрібно. Все можна зробити у звичайному контролері, який взагалі, як ви можете побачити, має дві стрічки смислового коду - 46 та 24. Тому, скільки би не умовляли програмістів використувати React - зрозуміло, що кода буде ЩОНАЙМЕНШЕ У СТО РАЗІВ БІЛЬШЕ. Якщо хтось хоче заперечити цьому - викладіть будь ласка своє рішення у каментах. У мої контролерах ДВІ стрічки смислового коду (WebApi контролер взагалі повністю пустий)- а скільки кода буде у ваших контролерах?
1: Public Class HomeController2: Inherits System.Web.Mvc.Controller3:
4: Function Index() As ActionResult5: Return View()6: End Function7:
8: Function Log() As ActionResult9: Return PartialView("Log")10: End Function11:
12: Function Data() As ActionResult13: Return PartialView("Data")14: End Function15:
16: Function AddData() As ActionResult17: Return PartialView("AddData")18: End Function19:
20: Function DelData(id As String) As ActionResult21: Try22: Dim i As String = id.Replace("del", ")23: Dim db1 As New GruveoDBDataContext24: db1.DelProduct(i)
25: Catch ex As Exception26: Return PartialView("Err1", ex.Message)27: End Try28: Return Nothing29: End Function30:
31: Function UpdData(id As String) As ActionResult32: Try33: ' Dim i As String = id.Replace("upd", ")34: ' Dim db1 As New GruveoDBDataContext35: ' db1.DelProduct(i)36: Catch ex As Exception37: ' Return PartialView("Err1", ex.Message)38: End Try39: Return Nothing40: End Function41:
42: <HttpPost()>
43: Function AddData(Prm As FormCollection) As ActionResult44: Try45: Dim db1 As New GruveoDBDataContext46: db1.AddProduct(Prm("txt"), Prm("price"), Prm("descript"))47: Catch ex As Exception48: Return PartialView("Err1", ex.Message)49: End Try50: Return Nothing51: End Function52:
53: End Class
Тепер подивимося, де взагалі прихований код проєкту. Сторінка Index теж пуста, там тільки плашка, яка підгружає смислову частку сайта.
А ось меню вже має дещо! Зверніть увагу, що я описую тут свій реальній процесс роботи, зрозуміло що після цих чорновіків потрібно зробити рефакторінг, ну хоча би стилі винести у окремі файли.
Як бачите, і тут ніякого Reacta нам не потрібно, все AJAX-меню зроблено всього двома AJAX-викликами та хандлером Refresh в одну стрічку. Хто небудь може зробити це ще коротше? Покажіть мені будь ласка у каментах.
1: @Code
2: ViewData("Title") = "Start page"
3: Layout = Nothing
4: End Code
5: <div id="menu">6:
7: @Ajax.ActionLink("Data", "Data", Nothing, New AjaxOptions() With {
8: .HttpMethod = "GET",
9: .InsertionMode = InsertionMode.Replace,
10: .UpdateTargetId = "content"
11: }, New With {
12: .id = "menudata"})
13:
14: @Ajax.ActionLink("Log", "Log", Nothing, New AjaxOptions() With {
15: .HttpMethod = "GET",
16: .InsertionMode = InsertionMode.Replace,
17: .UpdateTargetId = "content"
18: }, New With {
19: .id = "menulog"})
20:
21: </div>22:
23: <style>24: #menudata
25: {
26: color:Black;
27: width: 100px;
28: background-color: #d9ecf3;
29: margin: 5px;
30: font-size:large;
31: }
32: #menulog
33: {
34: color:Black;
35: width: 100px;
36: background-color: #d9ecf3;
37: margin: 5px;
38: font-size:large;
39: }
40: #adddata
41: {
42: color:Black;
43: font-size:large;
44: }
45: #sendpostdata
46: {
47: color:Black;
48: font-size:large;
49: }
50: .deldata
51: {
52: color:Black;
53: }
54: .upddata
55: {
56: color:Black;
57: }
58:
59: </style>60:
61:
62:
63: <div id="content"></div>64: <div id="errpanel"></div>65:
66: <script type="text/javascript">67: $(document).ready(function () {68: $("#menudata").bind("click", function () {69: $("#menulog").css("background-color", "#d9ecf3");70: $("#menudata").css("background-color", "#7ac0da");71: });
72: $("#menulog").bind("click", function () {73: $("#menudata").css("background-color", "#d9ecf3");74: $("#menulog").css("background-color", "#7ac0da");75: });
76: refresh1 = function () {77: $.ajax({
78: url: '/Home/Data',79: type: 'GET',80: success: function (e) {81: $("#content").replaceWith(e);82: }
83: })
84: };
85: });
86: </script>87:
88:
89: <script type="text/javascript">90: $(document).ready(function () {91: $("#adddata").click(function (e) {92: e.preventDefault();
93: $("#adddata").hide();94: });
95: });
96: </script>
Далі є вьюха Log, яка крутить інклуд з одним фрагментом даних Log.
1: @Code
2: ViewData("Title") = "Log"3: Layout = Nothing4: End Code5:
6: <div id="content2" style="background-color:#7ac0da;width:100%;padding: 10px;" >7:
8: @Code
9: Try10: @: <table border="1">11: Dim db1 As New SigmaTest.GruveoDBDataContext12: Dim AllLog = (From X In db1.SpaTestLogs Select X Order By X.i Descending).ToList13: For i As Integer = 0 To AllLog.Count - 114: If i Mod 2 = 0 Then15: @: <tr>
16: Else17: @: <tr style="background-color:#d9ecf3">18: End If19: Html.RenderPartial("OneLog", AllLog(i))20: @: </tr>
21: Next22: @: </table>
23: Catch ex As Exception24: @ex.Message
25: End Try26: End Code27:
28: </div>
1: <td>2: @Model.OperType
3: </td>4: <td>5: @Model.date
6: </td>7: <td>8: @Model.txt
9: </td>10: <td>11: @String.Format("{0:N2}",Model.Price)
12: </td>13: <td>14: @Model.Descript
15: </td>
З датою трошки складніше, по-перше є вьюха, яка додає дані до бази:
1: <div id="datapanel">2: <form id="addform" action="#" method="post">3: <input type="text" name="txt" id="txt" /><span style="color: Red">*</span><br />4: <input type="text" name="price" id="price" /><span style="color: Red">*</span><br />5: <input type="text" name="descript" id="descript" />6: </form>7: <a href="#" id="sendpostdata">(send)</a><br />8: </div>9:
10: <script type="text/javascript" >11:
12: $(document).ready(function () {13: $("#sendpostdata").click(function (e) {14: e.preventDefault();
15: var poststring = $("#addform").serialize();16: var ret = '';17: $.ajax({
18: url: '/Home/Adddata',19: data: poststring,
20: type: 'POST',21: success: function (data) {22: ret = data;
23: }
24: });
25:
26: refresh1();
27: $("#datapanel").hide();28: $("#adddata").show();29: $("#errpanel").replaceWith(ret);30: });
31: });
32: </script>33:
34:
А по-друге, дані потрібно оновлювати, показувати, та видаляти. Це робиться вьюхой Data.
1: @Code
2: ViewData("Title") = "Data"
3: Layout = Nothing
4: End Code
5: <div id="content" style="background-color: #7ac0da; width: 100%; padding: 10px;">6: @Code
7: Try
8: @: <table border="1">9: Dim db1 As New SigmaTest.GruveoDBDataContext10: Dim AllProd = (From X In db1.SpaTestProducts Select X Order By X.i Descending).ToList11: For i As Integer = 0 To AllProd.Count - 112: If i Mod 2 = 0 Then13: @: <tr>
14: Else15: @: <tr style="background-color:#d9ecf3">16: End If17: Html.RenderPartial("OneData", AllProd(i))18: @: </tr>
19: Next20: @: </table>
21: Catch ex As Exception22: @ex.Message
23: End Try24: End Code25: <br />
26:
27:
28: @Ajax.ActionLink("(new)", "AddData", Nothing, New AjaxOptions() With {
29: .HttpMethod = "GET",
30: .InsertionMode = InsertionMode.Replace,
31: .UpdateTargetId = "addplace"
32: }, New With {
33: .id = "adddata"})
34:
35: <div id="addplace"></div>36:
37: <script type="text/javascript" >38: $(document).ready(function () {39: $(".deldata").bind("click", function (e) {40: e.preventDefault();
41: var id = this.id;42: $.ajax({
43: url: '/Home/DelData',44: type: 'GET',45: data: { "id": id },46: success: function (data) {47: refresh1();
48: }
49: });
50: });
51: $(".upddata").bind("click", function (e) {52: e.preventDefault();
53: var id = this.id;54: $.ajax({
55: url: '/Home/UpdData',56: type: 'GET',57: data: { "id": id },58: success: function (data) {59: refresh1();
60: }
61: });
62: });
63: });
64: </script>65:
66:
67: </div>
1: <td>2: <a class="deldata" id="del@(Model.i)" href="#">(del)</a>3: </td>4: @*<td>5: <a class="upddata" id="upd@(Model.i)" href="#">(upd)</a>6: </td>*@7: <td>8: @Model.Txt
9: </td>10: <td>11: @String.Format("{0:N2}",Model.Price)
12: </td>13: <td>14: @Model.Descript
15: </td>
Як бачите, все це не просто, а надзвичайно просто! Ні в яке порівняння з новими замороченими технологіями, якими нібито навмисно намагаються задрочити програмістів різноманітні рекламні агентства, поширюючи маячню (нібито це дуже модернові та сучасні технології) - ніякого порівняння зі старими, перевіреними технологіями немає.
Можливо, я ще знайду вільний час і додам на цю сторінку ще декілька тестів, які я виконав в останні часи.
- Mій пост на Linkedin про тести.
- Ще декілька тестів, які я проходив на АпВорке: HTML5, JavaScript, SQL
- English spelling, English to russian translation, Russian to English translation
- Ще якийсь невеличкий тест на використання масивів.