کتابخانه scrollReveal.js
کتابخانه boxLoader-Jquery-Plugin
قبل از آن بد نیست که بدانید چرا این ابزار T4 نام گرفته !
T4 مخفف Text Template Transformation Toolkit میباشد (TTTT). شکل زیر مراحل اجرای یک کد Text Template را توسط T4 نشان میدهد:
برای آزمایش، یک فایل متنی کنار فایل TextTransform.exe با نام Text2.tt ایجاد نمایید و کد زیر را در داخل آن بنویسید:
<#@ template debug="true" hostspecific="false" language="C#" #> <#@ output extension=".txt" #> <#@ import namespace="System.Diagnostics" #> Report In : <#= DateTime.Now #> <# Process[] Procs = Process.GetProcesses(); for (int i = 0; i < Procs.Length; i++) { string Pstr = Procs[i].ProcessName + " -|- " + Procs[i].Id + Environment.NewLine ; #><#= Pstr #><# } #>
برای تولید فایل خروجی، دستور زیر را در cmd.exe اجرا کنید :
نکته: اگر User شما به این پوشه دسترسی ندارد و کاربر Admin نیستید احتمالا به مشکل بر میخورد. میتوانید فایل TextTransform.exe را در مکان دیگری قرار دهید و دستور را از آن محل اجرا کنید و یا برای پوشهی مذکور دسترسی ایجاد نمایید.
اگر میخواهید بیشتر در مورد معماری T4 بدانید بهتر است مقاله زیر را مطالعه کنید:
http://www.olegsych.com/2008/05/t4-architecture/
نکته دیگر این که برای Visual Studio، ابزارهایی جهت بهبود کار با Text Templateها وجود دارند که با جستجوی T4 Editor، نمونههایی از آنها را خواهید یافت. tangible T4 Editor نمونه ای از این Pluginها میباشد که به Visual Studio افزوده میگردد و یا یک پروژه Open Source هم برای آشنایی بسیار بیشتر با T4 در t4toolbox.codeplex.com وجود دارد که میتوانید مشاهده کنید.در این مقاله قصد داریم با استفاده از جاوااسکریپت خالص، یک برنامهی ساده را با الگوی MVC انجام دهیم. این برنامه، عملیات CRUD را پیاده سازی میکند و تنها به سه فایل index.html , script.js , style.css نیاز دارد و از هیچ کتابخانه یا فریم ورک دیگری در آن استفاده نمیکنیم.
- M مخفف Model میباشد و کار مدیریت دادهها را بر عهده دارد.
- V مخفف View میباشد و وظیفهی نمایش دادهها به کاربر را بر عهده دارد.
- C مخفف Controller میباشد و پل ارتباطی بین Model و View میباشد و مدیریت درخواستها را بر عهده دارد.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>الگوی MVC در جاوااسکریپت</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="root"></div> <script src="script.js"></script> </body> </html>
*, *::before, *::after { box-sizing: border-box } html { color: #444; } #root { max-width: 450px; margin: 2rem auto; padding: 0 1rem; } form { display: flex; margin-bottom: 2rem; } [type="text"], button { display: inline-block; -webkit-appearance: none; padding: .5rem 1rem; border: 2px solid #ccc; border-radius: 4px; } button { cursor: pointer; background: #007bff; color: white; border: 2px solid #007bff; margin: 0 .5rem; } [type="text"] { width: 100%; } [type="text"]:active, [type="text"]:focus { outline: 0; border: 2px solid #007bff; } [type="checkbox"] { margin-right: 1rem; } h1 { color: #222; } ul { padding: 0; } li { display: flex; align-items: center; padding: 1rem; margin-bottom: 1rem; background: #f4f4f4; border-radius: 4px; } li span { display: inline-block; padding: .5rem; width: 250px; border-radius: 4px; border: 2px solid transparent; } li span:hover { background: rgba(179, 215, 255, 0.52); } li span:focus { outline: 0; border: 2px solid #007bff; background: rgba(179, 207, 255, 0.52) }
class Model { constructor() {} } class View { constructor() {} } class Controller { constructor(model, view) { this.model = model this.view = view } } const app = new Controller(new Model(), new View())
class Model { constructor() { // یک آرایه از اطلاعات پیش فرض this.todos = [{ id: 1, text: 'Run a marathon', complete: false }, { id: 2, text: 'Plant a garden', complete: false }, ] } // متدی برای افزودن آیتم جدید به آرایه addTodo(todoText) { const todo = { id: this.todos.length > 0 ? this.todos[this.todos.length - 1].id + 1 : 1, text: todoText, complete: false, } this.todos.push(todo) } // متدی برای بروزسانی آیتم مورد نظر editTodo(id, updatedText) { this.todos = this.todos.map(todo => todo.id === id ? { id: todo.id, text: updatedText, complete: todo.complete } : todo ) } // انجام میدهد filter با استفاده از متد id تابعی که عملیات حذف را بوسیله فیلد deleteTodo(id) { this.todos = this.todos.filter(todo => todo.id !== id) } // متدی که در آن مشخص میکنیم کار مد نظرانجام شده یا خیر toggleTodo(id) { this.todos = this.todos.map(todo => todo.id === id ? { id: todo.id, text: todo.text, complete: !todo.complete } : todo ) } }
app.model.addTodo('Take a nap') console.log(app.model.todos)
در حال حاضر با هر بار reload شدن صفحه، فقط اطلاعات پیش فرض، درون آرایه todos قرار میگیرد؛ ولی در ادامه آن را در local storage ذخیره میکنیم.
برای ساختن قسمت View، از جاوااسکریپت استفاده میکنیم و DOM را تغییر میدهیم. البته اینکار را بدون استفاده از JSX و یا یک templating language انجام خواهیم داد. قسمتهای دیگر برنامه مانند Controller و Model نباید درگیر تغییرات DOM یا CSS یا عناصر HTML باشند و تمام این موارد توسط View هندل میشود. کد View به نحو زیر خواهد بود:
class View { constructor() {} // ایجاد یک المنت با کلاسهای استایل دلخواه createElement(tag, className) { const element = document.createElement(tag) if (className) element.classList.add(className) return element } // DOM انتخاب و گرفتن آیتمی خاص از getElement(selector) { const element = document.querySelector(selector) return element } }
سپس قسمت سازنده کلاس View را تغییر میدهیم و تمام المنتهای مورد نیاز را در آن ایجاد میکنیم:
- ارجاعی به المنتی با آیدی root
- تگ h1 برای عنوان
- یک form، input و دکمهای برای افزودن آیتمی جدید به آرایهی todos
- یک المنت ul برای نمایش آیتمهای todos
constructor() { // root ارجاعی به المنتی با آیدی this.app = this.getElement('#root') // عنوان برنامه this.title = this.createElement('h1') this.title.textContent = 'Todos' // فرم ، اینپوت ورودی و دکمه this.form = this.createElement('form') this.input = this.createElement('input') this.input.type = 'text' this.input.placeholder = 'Add todo' this.input.name = 'todo' this.submitButton = this.createElement('button') this.submitButton.textContent = 'Submit' // برای نمایش عناط آرایه یا همان لیست کارها this.todoList = this.createElement('ul', 'todo-list') // افزودن اینپوت ورودی و دکمه به فرم this.form.append(this.input, this.submitButton) // ایجاد شده است app که اینجا ارجاعی به آن بنام root اضافه کردن تمام آیتمهای بالا در المنتی با آیدی this.app.append(this.title, this.form, this.todoList) }
get _todoText() { return this.input.value } _resetInput() { this.input.value = '' }
displayTodos(todos){ //... }
متد displayTodos یک المنت ul و liهایی را به تعداد عناصر todos ایجاد میکند و آنها را نمایش میدهد. هر زمانکه تغییراتی مانند اضافه شدن، حذف و ویرایش در todos صورت گیرد، این متد دوباره فراخوانی میشود و لیست جدید را نمایش میدهد. محتوای متد dispayTodos به شکل زیر خواهد بود:
displayTodos(todos) { // حذف تمام نودها while (this.todoList.firstChild) { this.todoList.removeChild(this.todoList.firstChild) } // اگر هیچ آیتمی در آرایه نبود این پاراگراف با متن پیش فرض نمایش داده میشود if (todos.length === 0) { const p = this.createElement('p') p.textContent = 'Nothing to do! Add a task?' this.todoList.append(p) } else { // وعناصرمربوطه را ایجاد میکند liاگه درون آرایه آیتمی قرار دارد پس به ازای آن یک عنصر todos.forEach(todo => { const li = this.createElement('li') li.id = todo.id const checkbox = this.createElement('input') checkbox.type = 'checkbox' checkbox.checked = todo.complete const span = this.createElement('span') span.contentEditable = true span.classList.add('editable') if (todo.complete) { const strike = this.createElement('s') strike.textContent = todo.text span.append(strike) } else { span.textContent = todo.text } const deleteButton = this.createElement('button', 'delete') deleteButton.textContent = 'Delete' li.append(checkbox, span, deleteButton) // نود ایجاد شده به لیست اضافه میکند this.todoList.append(li) }) } // برای خطایابی و نمایش در کنسول console.log(todos) }
در نهایت قسمت Controller را که پل ارتباطی بین View و Model میباشد، کامل میکنیم. اولین تغییراتی که در کلاس Controller ایجاد میکنیم، استفاده از متد displayTodos در سازندهی این کلاس میباشد و با هر بار تغییر این متد، دوباره فراخوانی میشود:
class Controller { constructor(model, view) { this.model = model this.view = view // نمایش اطلاعات پیش فرض this.onTodoListChanged(this.model.todos) } onTodoListChanged = todos => { this.view.displayTodos(todos) } }
چهار تابعی را که در قسمت Model ایجاد نمودیم و کار ویرایش، حذف، افزودن و اتمام کار را انجام میدادند، در کلاس کنترلر آنها را هندل میکنیم و زمانیکه کاربر دکمهای را برای افزودن یا تیک حذف آیتمی، زد، تابع مربوطه توسط کنترلر در Model فراخوانی شود:
handleAddTodo = todoText => { this.model.addTodo(todoText) } handleEditTodo = (id, todoText) => { this.model.editTodo(id, todoText) } handleDeleteTodo = id => { this.model.deleteTodo(id) } handleToggleTodo = id => { this.model.toggleTodo(id) }
چون کنترلر نمیتواند بصورت مستقیم فراخوانی شود و این توابع باید درون DOM تنظیم شوند تا به ازای رخدادهایی همچون click و change، فراخوانی شوند. پس از این توابع در قسمت View استفاده میکنیم و به کلاس View، موارد زیر را اضافه میکنیم:
bindAddTodo(handler) { this.form.addEventListener('submit', event => { event.preventDefault() if (this._todoText) { handler(this._todoText) this._resetInput() } }) } bindDeleteTodo(handler) { this.todoList.addEventListener('click', event => { if (event.target.className === 'delete') { const id = parseInt(event.target.parentElement.id) handler(id) } }) } bindToggleTodo(handler) { this.todoList.addEventListener('change', event => { if (event.target.type === 'checkbox') { const id = parseInt(event.target.parentElement.id) handler(id) } }) }
برای bind کردن این متدها در کلاس Controller، کدهای زیر را اضافه میکنیم:
this.view.bindAddTodo(this.handleAddTodo) this.view.bindDeleteTodo(this.handleDeleteTodo) this.view.bindToggleTodo(this.handleToggleTodo)
برای ذخیره اطلاعات در local storage، در سازنده کلاس Model، کد زیر را اضافه میکنیم:
this.todos = JSON.parse(localStorage.getItem('todos')) || []
متد دیگری هم در کلاس Model برای بهروز رسانی مقادیر local storage قرار میدهیم:
_commit(todos) { this.onTodoListChanged(todos) localStorage.setItem('todos', JSON.stringify(todos)) }
متدی هم برای تغییراتی که هر زمان بر روی todos اتفاق میافتد، فراخوانی شود:
deleteTodo(id) { this.todos = this.todos.filter(todo => todo.id !== id) this._commit(this.todos) }
این مقاله صرفا جهت آشنایی و نمونه کدی از پیاده سازی الگوی MVC در جاوااسکریپت میباشد.
اگر هدر X-Content-Type-Options را به nosniff تنظیم کرده باشید، کروم 65 از اجرای فایلهای اسکریپت با خطای زیر سر باز میزند:
Refused to execute script from '<URL>' because its MIME type ('') is not executable, and strict MIME type checking is enabled.
برای رفع آن باید به صورت زیر content-type صحیحی را به فایلهای اسکریپت تولیدی نسبت داد:
var provider = new FileExtensionContentTypeProvider(); app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider });
متدهای توکار استفاده از نوع دادهای XML - قسمت اول
- query : xml را به عنوان ورودی گرفته و نهایتا یک خروجی XML دیگر را بر میگرداند.
- exist : خروجی bit دارد؛ true یا false. ورودی آن یک XQuery است.
- value : یک خروجی SQL Type را ارائه میدهد.
- nodes : خروجی جدولی دارد.
- modify : برای تغییر اطلاعات بکار میرود.
استفاده از متد exist به عنوان جایگزین سبک وزن XML Schema
یکی از کاربردهای متد exist، تعریف قید بر روی یک ستون XML ایی جدول است. این روش، راه حل دوم و سادهای است بجای استفاده از XML Schema برای ارزیابی و اعتبارسنجی کل سند. پیشنیاز اینکار، تعریف قید مدنظر توسط یک تابع جدید است:
CREATE FUNCTION dbo.checkPerson(@data XML) RETURNS BIT WITH SCHEMABINDING AS BEGIN RETURN @data.exist('/people/person') END GO CREATE TABLE tblXML ( id INT PRIMARY KEY, doc XML CHECK(dbo.checkPerson(doc)=1) ) GO
اکنون برای آزمایش آن خواهیم داشت:
INSERT INTO tblXML (id, doc) VALUES ( 1, '<people><person name="Vahid"/></people>' ) INSERT INTO tblXML (id, doc) VALUES ( 2, '<people><emp name="Vahid"/></people>' )
The INSERT statement conflicted with the CHECK constraint "CK__tblXML__doc__060DEAE8". The conflict occurred in database "testdb", table "dbo.tblXML", column 'doc'. The statement has been terminated.
استفاده از متد value برای دریافت اطلاعات
با کاربرد مقدماتی متد value در بازگشت یک مقدار scalar در قسمتهای قبل آشنا شدیم. در ادامه مثالهای کاربردیتر را بررسی خواهیم کرد.
ابتدا جدول زیر را با یک ستون XML در آن درنظر بگیرید:
CREATE TABLE xml_tab ( id INT IDENTITY PRIMARY KEY, xml_col XML )
INSERT INTO xml_tab VALUES ('<people><person name="Vahid"/></people>') INSERT INTO xml_tab VALUES ('<people><person name="Farid"/></people>')
SELECT id, xml_col.value('(/people/person/@name)[1]', 'varchar(50)') AS name FROM xml_tab
یک نکته
اگر نیاز به خروجی از نوع XML است، بهتر است از متد query که در دو قسمت قبل بررسی شد، استفاده گردد. خروجی متد query همیشه یک untyped XML است یا نال. البته میتوان خروجی آنرا به یک typed XML دارای Schema نیز نسبت داد. در اینجا اعتبارسنجی در حین انتساب صورت خواهد گرفت.
استفاده از متد value برای تعریف قیود
از متد value همچنین میتوان برای تعریف قیود پیشرفته نیز استفاده کرد. برای مثال فرض کنیم میخواهیم ویژگی Id سند XML در حال ذخیره شدن، حتما مساوی ستون Id جدول باشد. برای این منظور ابتدا نیاز است همانند قبل یک تابع جدید را ایجاد نمائیم:
CREATE FUNCTION getIdValue(@doc XML) RETURNS int WITH SCHEMABINDING AS BEGIN RETURN @doc.value('/*[1]/@Id', 'int') END
سپس از این تابع در عبارت check برای مقایسه ویژگی Id سند XML در حال ذخیره شدن و id ردیف جاری استفاده میشود:
CREATE TABLE docs_tab ( id INT PRIMARY KEY, doc XML, CONSTRAINT id_chk CHECK(dbo.getIdValue(doc)=id) )
در ادامه برای آزمایش آن خواهیم داشت:
INSERT INTO docs_tab (id, doc) VALUES ( 1, '<Invoice Id="1"/>' ) INSERT INTO docs_tab (id, doc) VALUES ( 2, '<Invoice Id="1"/>' )
The INSERT statement conflicted with the CHECK constraint "id_chk". The conflict occurred in database "testdb", table "dbo.docs_tab". The statement has been terminated.
استفاده از متد value برای تعریف primary key
پیشتر عنوان شد که از فیلدهای XML نمیتوان به عنوان کلید یک جدول استفاده کرد؛ چون امکان مقایسهی محتوای کل آنها وجود ندارد. اما با استفاده از متد value میتوان مقدار دریافتی را به عنوان یک کلید اصلی محاسبه شده، ثبت کرد:
CREATE TABLE Invoices ( doc XML, id AS dbo.getIdValue(doc) PERSISTED PRIMARY KEY )
برای آزمایش آن سعی میکنیم دو رکورد را که حاوی ویژگی id برابری هستند، ثبت کنیم:
INSERT INTO Invoices VALUES ( '<Invoice Id="1"/>' ) INSERT INTO Invoices VALUES ( '<Invoice Id="1"/>' )
Violation of PRIMARY KEY constraint 'PK__Invoices__3213E83F145C0A3F'. Cannot insert duplicate key in object 'dbo.Invoices'. The duplicate key value is (1). The statement has been terminated.
توابع دسترسی به مقدار دادهها در XQuery
تابع data ، string و text برای دسترسی به مقدار دادهها در XQuery پیش بینی شدهاند.
اگر سعی کنیم مثال زیر را اجرا نمائیم:
DECLARE @doc XML SET @doc = '<foo bar="baz" />' SELECT @doc.query('/foo/@bar')
XQuery [query()]: Attribute may not appear outside of an element
DECLARE @doc XML SET @doc = '<foo bar="baz" />' SELECT @doc.query('data(/foo/@bar)')
DECLARE @x XML SET @x = '<x>hello<y>world</y></x><x>again</x>' SELECT @x.query('data(/*)')
اما اگر همین مثال را با متد string اجرا کنیم:
DECLARE @x XML SET @x = '<x>hello<y>world</y></x><x>again</x>' SELECT @x.query('string(/*)')
XQuery [query()]: 'string()' requires a singleton (or empty sequence), found operand of type 'element(*,xdt:untyped) *'
SELECT @x.query('string(/*[1])')
برای دریافت تمام کلمات توسط متد string میتوان از اسلش کمک گرفت:
SELECT @x.query('string(/)')
نمونهی دیگر آن مثال زیر است:
DECLARE @x XML = '<age>12</age>' SELECT @x.query('string(/age[1])')
متد text اندکی متفاوت عمل میکند. برای بررسی آن، ابتدا یک schema collection جدید را تعریف میکنیم که داری تک المانی رشتهای است به نام Root.
CREATE XML SCHEMA COLLECTION root_el AS '<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:geo"> <xs:element name="Root" type="xs:string" /> </xs:schema> ' GO
DECLARE @xmlDoc XML SET @xmlDoc = '<g:Root xmlns:g="urn:geo">datadata...</g:Root>' SELECT @xmlDoc.query(' declare namespace g="urn:geo"; /g:Root/text() ')
DECLARE @xmlDoc XML(root_el) SET @xmlDoc = '<g:Root xmlns:g="urn:geo">datadata...</g:Root>' SELECT @xmlDoc.query(' declare namespace g="urn:geo"; /g:Root[1]/text() ')
XQuery [query()]: 'text()' is not supported on simple typed or 'http://www.w3.org/2001/XMLSchema#anyType' elements, found 'element(g{urn:geo}:Root,xs:string) *'.
DECLARE @xmlDoc XML(root_el) SET @xmlDoc = '<g:Root xmlns:g="urn:geo">datadata...</g:Root>' SELECT @xmlDoc.query(' declare namespace g="urn:geo"; data(/g:Root[1]) ')
data(/age/text())
کتابخانه tether
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت دوازدهم- یکپارچه سازی با اکانت گوگل
ثبت یک برنامهی جدید در گوگل
اگر بخواهیم از گوگل به عنوان یک IDP ثالث در IdentityServer استفاده کنیم، نیاز است در ابتدا برنامهی IDP خود را به آن معرفی و در آنجا ثبت کنیم. برای این منظور مراحل زیر را طی خواهیم کرد:
1- مراجعه به developer console گوگل و ایجاد یک پروژهی جدید
https://console.developers.google.com
در صفحهی باز شده، بر روی دکمهی select project در صفحه و یا لینک select a project در نوار ابزار آن کلیک کنید. در اینجا دکمهی new project و یا create را مشاهده خواهید کرد. هر دوی این مفاهیم به صفحهی زیر ختم میشوند:
در اینجا نامی دلخواه را وارد کرده و بر روی دکمهی create کلیک کنید.
2- فعالسازی API بر روی این پروژهی جدید
در ادامه بر روی لینک Enable APIs And Services کلیک کنید و سپس google+ api را جستجو نمائید.
پس از ظاهر شدن آن، این گزینه را انتخاب و در صفحهی بعدی، آنرا با کلیک بر روی دکمهی enable، فعال کنید.
3- ایجاد credentials
در اینجا بر روی دکمهی create credentials کلیک کرده و در صفحهی بعدی، این سه گزینه را با مقادیر مشخص شده، تکمیل کنید:
• Which API are you using? – Google+ API • Where will you be calling the API from? – Web server (e.g. node.js, Tomcat) • What data will you be accessing? – User data
• نام: همان مقدار پیشفرض آن
• Authorized JavaScript origins: آنرا خالی بگذارید.
• Authorized redirect URIs: این مورد همان callback address مربوط به IDP ما است که در اینجا آنرا با آدرس زیر مقدار دهی خواهیم کرد.
https://localhost:6001/signin-google
سپس در ذیل این صفحه بر روی دکمهی «Create OAuth 2.0 Client ID» کلیک کنید تا به صفحهی «Set up the OAuth 2.0 consent screen» بعدی هدایت شوید. در اینجا دو گزینهی آنرا به صورت زیر تکمیل کنید:
- Email address: همان آدرس ایمیل واقعی شما است.
- Product name shown to users: یک نام دلخواه است. نام برنامهی خود را برای نمونه ImageGallery وارد کنید.
برای ادامه بر روی دکمهی Continue کلیک نمائید.
4- دریافت credentials
در پایان این گردش کاری، به صفحهی نهایی «Download credentials» میرسیم. در اینجا بر روی دکمهی download کلیک کنید تا ClientId و ClientSecret خود را توسط فایلی به نام client_id.json دریافت نمائید.
سپس بر روی دکمهی Done در ذیل صفحه کلیک کنید تا این پروسه خاتمه یابد.
تنظیم برنامهی IDP برای استفادهی از محتویات فایل client_id.json
پس از پایان عملیات ایجاد یک برنامهی جدید در گوگل و فعالسازی Google+ API در آن، یک فایل client_id.json را دریافت میکنیم که اطلاعات آن باید به صورت زیر به فایل آغازین برنامهی IDP اضافه شود:
الف) تکمیل فایل src\IDP\DNT.IDP\appsettings.json
{ "Authentication": { "Google": { "ClientId": "xxxx", "ClientSecret": "xxxx" } } }
ب) تکمیل اطلاعات گوگل در کلاس آغازین برنامه
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddAuthentication() .AddGoogle(authenticationScheme: "Google", configureOptions: options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = Configuration["Authentication:Google:ClientId"]; options.ClientSecret = Configuration["Authentication:Google:ClientSecret"]; }); }
- authenticationScheme تنظیم شده باید یک عبارت منحصربفرد باشد.
- همچنین SignInScheme یک چنین مقداری را در اصل دارد:
public const string ExternalCookieAuthenticationScheme = "idsrv.external";
آزمایش اعتبارسنجی کاربران توسط اکانت گوگل آنها
اکنون که تنظیمات اکانت گوگل به پایان رسید و همچنین به برنامه نیز معرفی شد، برنامهها را اجرا کنید. مشاهده خواهید کرد که امکان لاگین توسط اکانت گوگل نیز به صورت خودکار به صفحهی لاگین IDP ما اضافه شدهاست:
در اینجا با کلیک بر روی دکمهی گوگل، به صفحهی لاگین آن که به همراه نام برنامهی ما است و انتخاب اکانتی از آن هدایت میشویم:
پس از آن، از طرف گوگل به صورت خودکار به IDP (همان آدرسی که در فیلد Authorized redirect URIs وارد کردیم)، هدایت شده و callback رخداده، ما را به سمت صفحهی ثبت اطلاعات کاربر جدید هدایت میکند. این تنظیمات را در قسمت قبل ایجاد کردیم:
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class ExternalController : Controller { public async Task<IActionResult> Callback() { var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); if (user == null) { // user = AutoProvisionUser(provider, providerUserId, claims); var returnUrlAfterRegistration = Url.Action("Callback", new { returnUrl = returnUrl }); var continueWithUrl = Url.Action("RegisterUser", "UserRegistration" , new { returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId }); return Redirect(continueWithUrl); }
در اینجا نحوهی اصلاح اکشن متد Callback را جهت هدایت یک کاربر جدید به صفحهی ثبت نام و تکمیل اطلاعات مورد نیاز IDP را مشاهده میکنید.
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره میکند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد و به برنامه با این هویت جدید وارد میشود.
اتصال کاربر وارد شدهی از طریق یک IDP خارجی به اکانتی که هم اکنون در سطح IDP ما موجود است
تا اینجا اگر کاربری از طریق یک IDP خارجی به برنامه وارد شود، او را به صفحهی ثبت نام کاربر هدایت کرده و پس از دریافت اطلاعات او، اکانت خارجی او را به اکانتی جدید که در IDP خود ایجاد میکنیم، متصل خواهیم کرد. به همین جهت بار دومی که این کاربر به همین ترتیب وارد سایت میشود، دیگر صفحهی ثبت نام و تکمیل اطلاعات را مشاهده نمیکند. اما ممکن است کاربری که برای اولین بار از طریق یک IDP خارجی به سایت ما وارد شدهاست، هم اکنون دارای یک اکانت دیگری در سطح IDP ما باشد؛ در اینجا فقط اتصالی بین این دو صورت نگرفتهاست. بنابراین در این حالت بجای ایجاد یک اکانت جدید، بهتر است از همین اکانت موجود استفاده کرد و صرفا اتصال UserLogins او را تکمیل نمود.
به همین جهت ابتدا نیاز است لیست Claims بازگشتی از گوگل را بررسی کنیم:
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); foreach (var claim in claims) { _logger.LogInformation($"External provider[{provider}] info-> claim:{claim.Type}, value:{claim.Value}"); }
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name, value:Vahid N. External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname, value:Vahid External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname, value:N. External provider[Google] info-> claim:urn:google:profile, value:https://plus.google.com/105013528531611201860 External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, value:my.name@gmail.com
[HttpGet] public async Task<IActionResult> Callback() { // ... var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); if (user == null) { // user wasn't found by provider, but maybe one exists with the same email address? if (provider == "Google") { // email claim from Google var email = claims.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"); if (email != null) { var userByEmail = await _usersService.GetUserByEmailAsync(email.Value); if (userByEmail != null) { // add Google as a provider for this user await _usersService.AddUserLoginAsync(userByEmail.SubjectId, provider, providerUserId); // redirect to ExternalLoginCallback var continueWithUrlAfterAddingUserLogin = Url.Action("Callback", new {returnUrl = returnUrl}); return Redirect(continueWithUrlAfterAddingUserLogin); } } } var returnUrlAfterRegistration = Url.Action("Callback", new {returnUrl = returnUrl}); var continueWithUrl = Url.Action("RegisterUser", "UserRegistration", new {returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId}); return Redirect(continueWithUrl); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
دریافت و نصب AvalonEdit
برای نصب AvalonEdit میتوان دستور ذیل را در کنسول پاورشل نیوگت صادر کرد:
PM> install-package AvalonEdit
استفادهی مقدماتی از AvalonEdit
برای استفاده از این ویرایشگر ابتدا نیاز است فضای نام xmlns:avalonEdit تعریف شود. سپس کنترل avalonEdit:TextEditor در دسترس خواهد بود:
<Window x:Class="SyntaxHighlighter.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit" Title="MainWindow" Height="401" Width="617"> <Grid> <avalonEdit:TextEditor Name="txtCode" SyntaxHighlighting="C#" FontFamily="Consolas" FontSize="10pt"/> </Grid> </Window>
استفاده از AvalonEdit در برنامههای MVVM
خاصیت Text این ویرایشگر به صورت معمولی تعریف شده (DependencyProperty نیست) و امکان binding دو طرفه به آن وجود ندارد. به همین جهت نیاز است یک چنین DependencyProperty را به آن اضافه کرد:
using System; using System.Collections.Concurrent; using System.Reflection; using System.Windows; using System.Xml; using ICSharpCode.AvalonEdit; using ICSharpCode.AvalonEdit.Highlighting; using ICSharpCode.AvalonEdit.Highlighting.Xshd; namespace AvalonEditWpfTest.Controls { public class BindableAvalonTextEditor : TextEditor { public static readonly DependencyProperty BoundTextProperty = DependencyProperty.Register("BoundText", typeof(string), typeof(BindableAvalonTextEditor), new FrameworkPropertyMetadata(default(string), propertyChangedCallback)); public static string GetBoundText(DependencyObject obj) { return (string)obj.GetValue(BoundTextProperty); } public static void SetBoundText(DependencyObject obj, string value) { obj.SetValue(BoundTextProperty, value); } protected override void OnTextChanged(EventArgs e) { SetCurrentValue(BoundTextProperty, Text); base.OnTextChanged(e); } private static void propertyChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args) { var target = (BindableAvalonTextEditor)obj; var value = args.NewValue; if (value == null) return; if (string.IsNullOrWhiteSpace(target.Text) || !target.Text.Equals(args.NewValue.ToString())) { target.Text = args.NewValue.ToString(); } } } }
اکنون برای استفاده از این کنترل جدید که BindableAvalonTextEditor نام دارد، میتوان به نحو ذیل عمل کرد:
<Window x:Class="AvalonEditWpfTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:viewModels="clr-namespace:AvalonEditTests.ViewModels" xmlns:controls="clr-namespace:AvalonEditWpfTest.Controls" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <viewModels:MainWindowViewModel x:Key="MainWindowViewModel"/> </Window.Resources> <Grid DataContext="{Binding Source={StaticResource MainWindowViewModel}}"> <controls:BindableAvalonTextEditor BoundText="{Binding SourceCode, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" WordWrap="True" ShowLineNumbers="True" LineNumbersForeground="MediumSlateBlue" FontFamily="Consolas" VerticalScrollBarVisibility="Auto" Margin="3" HorizontalScrollBarVisibility="Auto" FontSize="10pt"/> </Grid> </Window>
افزودن syntax highlighting زبانهایی که به صورت رسمی پشتیبانی نمیشوند
به خاصیت SyntaxHighlighting این کنترل صرفا مقادیری را میتوان نسبت داد که به صورت توکار پشتیبانی میشوند. برای مثال#XML، C و امثال آن.
فرض کنید نیاز است SyntaxHighlighting زبان SQL را فعال کنیم. برای اینکار نیاز به فایلهای ویژهای است، با پسوند xshd. برای نمونه فایل sql-ce.xshd را در اینجا میتوانید مطالعه کنید. در آن یک سری واژههای کلیدی و حروفی که باید با رنگی متفاوت نمایش داده شوند، مشخص میگردند.
برای استفاده از فایل sql-ce.xshd باید به نحو ذیل عمل کرد:
الف) فایل sql-ce.xshd را به پروژه اضافه کرده و سپس در برگهی خواص آن در VS.NET، مقدار build action آنرا به embedded resource تغییر دهید.
ب) با استفاده از متد ذیل، این فایل مدفون شده در اسمبلی را گشوده و به متد HighlightingLoader.Load ارسال میکنیم:
private static IHighlightingDefinition getHighlightingDefinition(string resourceName) { if (string.IsNullOrWhiteSpace(resourceName)) throw new NullReferenceException("Please specify SyntaxHighlightingResourceName."); using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) { if (stream == null) throw new NullReferenceException(string.Format("{0} resource is null.", resourceName)); using (var reader = new XmlTextReader(stream)) { return HighlightingLoader.Load(reader, HighlightingManager.Instance); } } }
txtCode.SyntaxHighlighting = getHighlightingDefinition(resourceName);
برای سهولت استفاده از این قابلیت شاید بهتر باشد یک DependencyProperty دیگر به نام SyntaxHighlightingResourceName را به کنترل جدید BindableAvalonTextEditor اضافه کنیم:
using System; using System.Collections.Concurrent; using System.Reflection; using System.Windows; using System.Xml; using ICSharpCode.AvalonEdit; using ICSharpCode.AvalonEdit.Highlighting; using ICSharpCode.AvalonEdit.Highlighting.Xshd; namespace AvalonEditWpfTest.Controls { public class BindableAvalonTextEditor : TextEditor { public static readonly DependencyProperty SyntaxHighlightingResourceNameProperty = DependencyProperty.Register("SyntaxHighlightingResourceName", typeof(string), typeof(BindableAvalonTextEditor), new FrameworkPropertyMetadata(default(string), resourceNamePropertyChangedCallback)); public static string GetSyntaxHighlightingResourceName(DependencyObject obj) { return (string)obj.GetValue(SyntaxHighlightingResourceNameProperty); } public static void SetSyntaxHighlightingResourceName(DependencyObject obj, string value) { obj.SetValue(SyntaxHighlightingResourceNameProperty, value); } private static void loadHighlighter(TextEditor @this, string resourceName) { if (@this.SyntaxHighlighting != null) return; @this.SyntaxHighlighting = getHighlightingDefinition(resourceName); } private static void resourceNamePropertyChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args) { var target = (BindableAvalonTextEditor)obj; var value = args.NewValue; if (value == null) return; loadHighlighter(target, value.ToString()); } } }
استفاده از آن نیز به شکل زیر است:
<controls:BindableAvalonTextEditor SyntaxHighlightingResourceName = "AvalonEditWpfTest.Controls.sql-ce.xshd" />
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
AvalonEditWpfTest.zip