No provider for HttpClient هم یعنی تنظیمات اولیه ماژولها را کامل نکردید (در دو قسمت imports و exports).
No provider for HttpClient هم یعنی تنظیمات اولیه ماژولها را کامل نکردید (در دو قسمت imports و exports).
"tools": { "BundlerMinifier.Core": "2.1.258" },
No executable found matching command "dotnet-bundle" Version for package `BundlerMinifier.Core` could not be resolved.
[ { "outputFileName": "wwwroot/css/site.min.css", "inputFiles": [ "wwwroot/css/site.css" ] }, { "outputFileName": "wwwroot/js/site.min.js", "inputFiles": [ "bower_components/jquery/dist/jquery.min.js", "bower_components/jquery-validation/dist/jquery.validate.min.js", "bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js" ], "minify": { "enabled": true, "renameLocals": true }, "sourceMap": false } ]
"scripts": { "precompile": [ "dotnet bundle" ], "prepublish": [ "bower install" ], "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] }
<script src="~/js/site.min.js" asp-append-version="true" type="text/javascript"></script>
using System.IO; using System.Xml; using System.Xml.Serialization; namespace DNTViewer.Common.Toolkit { public static class Serializer { public static string Serialize<T>(T type) { var serializer = new XmlSerializer(type.GetType()); using (var stream = new MemoryStream()) { serializer.Serialize(stream, type); stream.Seek(0, SeekOrigin.Begin); using (var reader = new StreamReader(stream)) { return reader.ReadToEnd(); } } } } }
private string createXmlFile(string dir) { var xLinq = new XElement("ArrayOfPost", _blogPosts .AsNoTracking() .Include(x => x.Comments) .Include(x => x.User) .Include(x => x.Tags) .OrderBy(x => x.Id) .ToList() .Select(x => new XElement("Post", postXElement(x))) ); var xmlFile = Path.Combine(dir, "dot-net-tips-database.xml"); xLinq.Save(xmlFile); return xmlFile; } private static XElement[] postXElement(BlogPost x) { return new XElement[] { new XElement("Id", x.Id), new XElement("Title", x.Title), new XElement("Body", x.Body), new XElement("CreatedOn", x.CreatedOn), tagElement(x), new XElement("User", new XElement("Id", x.UserId.Value), new XElement("FriendlyName", x.User.FriendlyName)) }.Where(item => item != null).ToArray(); } private static XElement tagElement(BlogPost x) { var tags = x.Tags.Any() ? x.Tags.Select(y => new XElement("Tag", new XElement("Id", y.Id), new XElement("Name", y.Name))) .ToArray() : null; if (tags == null) return null; return new XElement("Tags", tags); }
using System.IO; using System.Xml; using System.Xml.Serialization; namespace DNTViewer.Common.Toolkit { public static class Serializer { public static T DeserializePath<T>(string xmlAddress) { using (var xmlReader = new XmlTextReader(xmlAddress)) { var serializer = new XmlSerializer(typeof(T)); return (T)serializer.Deserialize(xmlReader); } } } }
[assembly: OwinStartupAttribute(typeof(PPU.WebUi.Startup))]
در خیلی از مواقع workflowها به مرحلهای میرسند که احتیاج به دستوری از بیرون از فرآیند دارند. در هنگام انتظار، اگر به هر دلیلی workflow از حافظه حذف شود، امکان ادامه فرآیند وجود ندارد. اما میتوان با Persist (ذخیره) کردن آن، در زمان انتظار و فراخوانی مجدد آن در هنگام نیاز، این ریسک را برطرف نمود.
قصد دارم با این مثال، طریقه persist شدن یک workflow در زمانیکه نیاز به انتظار برای تایید دارد و فراخوانی آن از همان نقطه پس از تایید مربوطه را توضیح دهم.
ساختار اینترفیس کاربری ما WPF میباشد. پس در ابتدا یک پروژه از نوع WPF ایجاد میکنیم. اسم solution را PersistWF و اسم Project را PersistWF.UI انتخاب میکنیم.
در پروژه UI نام فایل MainWindow.xaml را به AddRequest.xaml تغییر میدهیم. همچنین اسم کلاس مربوطه را در codebehind
همین طور مقدار StartupUri را هم در app.xaml اصلاح میکنیم
StartupUri="AddRequest.xaml"
Reference های زیر رو هم به پروژه اضافه میکنیم
•System.Activities •System.Activities.DurableInstancing •System.Configuration •System.Data.Linq •System.Runtime.DurableInstancing •System.ServiceModel •System.ServiceModel.Activities •System.Workflow.ComponentModel •System.Runtime.DurableInstancing •System.Activities.DurableInstancing
قرار است کاربری ثبت نام کند، در فرایند ثبت، منتظر تایید یکی از مدیران قرار میگیرد. مدیر، لیست کاربران جدید را میبینید، یک کاربر را انتخاب میکند؛ مقادیر لازم را وارد میکند و سپس پروسه تایید را انجام میدهد که فراخوانی فرآیند مربوطه از همان قسمتیاست که منتظر تایید مانده است.
برای Persist کردن workflow از کلاس SqlWorkflowInstanceStore استفاده میکنم. این شی به connection ای به یک دیتابیس با یک ساختار معین احتیاج دارد. خوشبختانه اسکریپتهای مورد نیاز این ساختار در پوشه [Drive]:\Windows\Microsoft.NET\Framework\v4.0.30319\SQL\en وجود دارند. دو اسکریپت با نامهای SqlWorkflowInstanceStoreSchema و SqlWorkflowInstanceStoreLogic باید به ترتیب در دیتابیس اجرا شوند.
من یک دیتابیس با نام PersistWF ایجاد میکنم و اسکریپتها را بر روی آن اجرا میکنم. یک جدول هم برای نگهداری کاربران ثبت شده در همین دیتابیس ایجاد میکنم.
و شمایل دیتابیس ما پس از اجرا کردن اسکریپتها و ساختن جدول User بدین شکل است:
XAML زیر، ساختار فرم AddRequest میباشد که قرار است نقش UI برنامه را ایفا کند. آن را با XAMLهای پیش فرض عوض کنید.
<Window x:Class="PersistWF.UI.AddRequest" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="520" Width="550" Loaded="Window_Loaded"> <Grid MinWidth="300" MinHeight="100" Width="514"> <Label Height="30" Margin="5,10,10,10" Name="lblName" VerticalAlignment="Top" HorizontalAlignment="Left" Width="90" HorizontalContentAlignment="Right">Name:</Label> <Label Height="30" Margin="270,10,10,10" Name="lblPhone" VerticalAlignment="Top" HorizontalAlignment="Left" Width="90" HorizontalContentAlignment="Right">Phone Number:</Label> <Label Height="30" Margin="5,40,10,10" Name="lblEmail" VerticalAlignment="Top" HorizontalAlignment="Left" Width="90" HorizontalContentAlignment="Right">Email:</Label> <TextBox Height="25" Margin="100,10,10,10" Name="txtName" VerticalAlignment="Top" HorizontalAlignment="Left" Width="170" /> <TextBox Height="25" Margin="365,10,10,10" Name="txtPhone" VerticalAlignment="Top" HorizontalAlignment="Left" Width="100" /> <TextBox Height="25" Margin="100,40,10,10" Name="txtEmail" VerticalAlignment="Top" HorizontalAlignment="Left" Width="300" /> <Button Height="23" Margin="100,86,0,0" Name="brnRegister" VerticalAlignment="Top" HorizontalAlignment="Left" Width="70" Click="brnRegister_Click">Register</Button> <ListView x:Name="lstUsers" Margin="10,125,10,10" Height="145" VerticalAlignment="Top" ItemsSource="{Binding}" HorizontalContentAlignment="Center" SelectionChanged="lstUsers_SelectionChanged" > <ListView.View> <GridView> <GridViewColumn Header="Current User" Width="480"> <GridViewColumn.CellTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" Width="110"/> <TextBlock Text="{Binding Phone}" Width="70"/> <TextBlock Text="{Binding Email}" Width="130"/> <TextBlock Text="{Binding Status}" Width="70"/> <TextBlock Text="{Binding AcceptedBy}" Width="100"/> </StackPanel> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> </GridView> </ListView.View> </ListView> <Label Height="37" HorizontalAlignment="Stretch" Margin="10,272,5,10" Name="lblSelectedNotes" VerticalAlignment="Top" Visibility="Hidden" /> <Label Height="30" Margin="10,0,0,140" Name="lblAgent" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="40" HorizontalContentAlignment="Left" Visibility="Hidden">Admin Name:</Label> <TextBox Height="25" Margin="60,0,0,140" Name="txtAcceptedBy" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="190" Visibility="Hidden" /> <Button Height="25" Margin="270,0,0,140" Name="btnAccept" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="90" Click="btnAccept_Click" Visibility="Hidden">Accept</Button> <Label Height="27" HorizontalAlignment="Left" Margin="10,0,0,110" Name="lblEvent" VerticalAlignment="Bottom" Width="76">Event Log</Label> <ListBox Margin="12,0,5,12" Name="lstEvents" Height="100" VerticalAlignment="Bottom" FontStretch="Condensed" FontSize="10" /> </Grid> </Window>
اگر همه چیز مرتب باشد؛ ساختار فرم شما باید به این شکل
باشد
اکثر workflowها از activity معروف WrteLine استفاده میکنند که برای نمایش یک رشته به کار میرود. ما هم در workflow مثالمان از این Activity استفاده میکنیم. اما برای اینکه مقادیری که توسط این Activity ایجاد میشوند در کادر event log فرم خودمان نمایش داده شود؛ احتیاج داریم که یک TextWriter سفارشی برای خودمان ایجاد کنیم. اما قبل از آن یک کلاس static در پروژه ایجاد میکنیم که بتوانیم در هر قسمتی، به فرم دسترسی داشته باشیم.
کلاسی را با نام ApplicationInterface به پروژه اضافه کرده و یک Property استاتیک از جنس فرم AddRequest هم
برای آن تعریف میکنیم:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace PersistWF.UI { public static class ApplicationInterface { public static AddRequest _app { get; set; } } }
به Constructor کلاس موجود در فایل AddRequest.xaml.cs این خط کد رو اضافه میکنم
public AddRequest() { InitializeComponent(); ApplicationInterface._app = this; }
private void AddEvent(string szText) { lstEvents.Items.Add(szText); } public ListBox GetEventListBox() { return this.lstEvents; }
متد اول برای اضافه کردن یک event Log و متد دوم هم که کنسول لاگ را در اختیار درخواست کنندهاش قرار میدهد.
و حالا کلاس TextWriter سفارشیامان را مینویسیم. یک کلاس به نام ListBoxTextWriter به پروژه اضافه میکنیم که از TextWriter مشتق میشود و محتویات آنرا در زیر میبینید:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Controls; namespace PersistWF.UI { public class ListBoxTextWriter : TextWriter { const string textClosed = "This TextWriter must be opened before use"; private Encoding _encoding; private bool _isOpen = false; private ListBox _listBox; public ListBoxTextWriter() { // Get the static list box _listBox = ApplicationInterface._app.GetEventListBox(); if (_listBox != null) _isOpen = true; } public ListBoxTextWriter(ListBox listBox) { this._listBox = listBox; this._isOpen = true; } public override Encoding Encoding { get { if (_encoding == null) { _encoding = new UnicodeEncoding(false, false); } return _encoding; } } public override void Close() { this.Dispose(true); } protected override void Dispose(bool disposing) { this._isOpen = false; base.Dispose(disposing); } public override void Write(char value) { if (!this._isOpen) throw new ApplicationException(textClosed); ; this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(value.ToString()))); } public override void Write(string value) { if (!this._isOpen) throw new ApplicationException(textClosed); if (value != null) this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(value))); } public override void Write(char[] buffer, int index, int count) { String toAdd = ""; if (!this._isOpen) throw new ApplicationException(textClosed); ; if (buffer == null || index < 0 || count < 0) throw new ArgumentOutOfRangeException("buffer"); if ((buffer.Length - index) < count) throw new ArgumentException("The buffer is too small"); for (int i = 0; i < count; i++) toAdd += buffer[i]; this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(toAdd))); } } }
همان طور که میبینید کلاس ListBoxTextWriter از کلاس abstract TextWriter مشتق شده و پیاده سازی از متد Write را فراهم میکند تا یک رشته را به کنترل ListBox اضافه کنه. (البته سه تا از این متدها را Override میکنیم تا بتوانیم یک رشته، یک کاراکتر و یا آرایه ای از کاراکترها را به ListBox اضافه کنیم) در constructor پیشفرض از کلاس ApplicationInterface استفاده کردیم تا بتوانیم کنترل lstEvents را از فرم اصلی برنامه به دست بیاوریم. برای Add کردن از Dispatcher و متد BeginInvoke مرتبط با آن استفاده کردیم . این کار، متد را قادر میسازد حتی وقتیکه از یک thread متفاوت فراخوانی میشود، کار کند.
حالا میتوانیم از این کلاس، به عنوان مقدار خاصیت TextWriter برای WriteLine استفاده کنیم.
به کلاس ApplicationInterface برگردیم تا متد زیر را هم به آن اضافه کنیم
public static void AddEvent(String status) { if (_app != null) { new ListBoxTextWriter(_app.GetEventListBox()).WriteLine(status); } }
این هم از constructor دومی استفاده میکنه برای معرفی ListBox.
برای ارتباط با دیتابیس از LINQ to SQL استفاده میکنیم تا User رو ذخیره و بازیابی کنیم. به پروژه یک آیتم از نوع LINQ to SQL با نام UserData.dbml اضافه میکنیم. به دیتابیس متصل شده و جدول User رو به محیط Design میکشیم. در ادامه برای شی کلاس SQLWorkflowInstanceStore هم از همین Connectionstring استفاده میکنیم.
برای ایجاد workflow مورد نظر، به دو Activity سفارشی احتیاج داریم که باید خودمان ایجاد نماییم. یک پوشه با نام Activities به پروژه اضافه میکنم تا کلاسهای مورد نظر را آنجا ایجاد کنیم.
1. یک Activity برای ایجاد User
این Activity تعدادی پارامتر از نوع InArgument دارد که توسط آنها یک Instance از کلاس User ایجاد میکند و در حقیقت آن را به دیتابیس میفرستد و دخیره میکند. Connectionstring را هم میشود توسط یک آرگومان ورودی دیگر مقدار دهی کرد. یک آرگومان خروجی هم برای این Activity در نظر میگیریم تا User ایجاد شده را برگردانیم. روی پوشهی Activities کلیک راست میکنیم و Add - NewItem را انتخاب میکنیم. از لیست workflowها Template مربوط به CodeActivity را انتخاب کرده و یک CodeActivity با نام CreateUser ایجاد میکنیم
محتویات این کلاس را هم مانند زیر کامل میکنیم
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Activities; namespace PersistWF.UI.Activities { public sealed class CreateUser : CodeActivity { public InArgument<string> Name { get; set; } public InArgument<string> Email { get; set; } public InArgument<string> Phone { get; set; } public InArgument<string> ConnectionString { get; set; } public OutArgument<User> User { get; set; } protected override void Execute(CodeActivityContext context) { // ایجاد کاربر User user = new User(); user.Email = Email.Get(context); user.Name = Name.Get(context); user.Phone = Phone.Get(context); user.Status = "New"; user.WorkflowID = context.WorkflowInstanceId; UserDataDataContext db = new UserDataDataContext(ConnectionString.Get(context)); db.Users.InsertOnSubmit(user); db.SubmitChanges(); User.Set(context, user); } } }
متد Execute، توسط مقادیری که به عنوان پارامتر دریافت شده، یک شی از کلاس User ایجاد میکند و به کمک DataContext آنرا در دیتابیس دخیره کرده و در آخر User ذخیره شده را در اختیار پارامتر خروجی قرار میدهد.
1. یک Activity برای انتظار دریافت تایید
این Activity قرار است Workflow را Idle کند تا زمانیکه مدیر دستور تایید را با فراخوانی مجدد workflow از این همین
قسمت صادر نماید.
این Activity باید از NativeActivity مشتق شده و برای اینکه workflow را وادرا به معلق شدن کند کافیاست خاصیت CanInduceIdle را با مقدار برگشتی true , override کنیم.
مثل قسمت قبل یک CodeActivity ایجاد میکنیم. اینبار با نام WaitForAccept که محتویاتش را هم مانند زیر تغییر میدهیم.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Activities; using System.Workflow.ComponentModel; namespace PersistWF.UI.Activities { public sealed class WaitForAccept<T> : NativeActivity<T> { public WaitForAccept() :base() { } public string BookmarkName { get; set; } public OutArgument<T> Input { get; set; } protected override void Execute(NativeActivityContext context) { context.CreateBookmark(BookmarkName, new BookmarkCallback(this.Continue)); } private void Continue(NativeActivityContext context, Bookmark bookmark, object value) { Input.Set(context, (T)value); } protected override bool CanInduceIdle { get { return true; } } } }
برای پیاده سازی از طریق کد، یک کلاس با نام UserWF ایجاد میکنیم و محتویات workflow را مانند زیر پیاده سازی خواهیم کرد:
using PersistWF.UI.Activities; using System; using System.Activities; using System.Activities.Statements; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace PersistWF.UI { public sealed class UserWF : Activity { public InArgument<string> Name { get; set; } public InArgument<string> Email { get; set; } public InArgument<string> Phone { get; set; } public InArgument<string> ConnectionString { get; set; } public InArgument<TextWriter> Writer { get; set; } public UserWF() { Variable<User> User = new Variable<User> { Name = "User" }; this.Implementation = () => new Sequence { DisplayName = "EnterUser", Variables = { User }, Activities = { new CreateUser // 1. ایجاد کاربر با ورود پارامترهای ورودی { ConnectionString = new InArgument<string>(c=> ConnectionString.Get(c)), Email = new InArgument<string>(c=> Email.Get(c)), Name = new InArgument<string>(c=> Name.Get(c)), Phone = new InArgument<string>(c=> Phone.Get(c)), User = new OutArgument<User>(c=> User.Get(c)) }, new WriteLine // 2. لاگ مربوط به دخیره کاربر { TextWriter = new InArgument<TextWriter>(c=> Writer.Get(c)), Text = new InArgument<string>(c=> string.Format("User {0} Registered and waiting for Accept", Name.Get(c) ) ) }, new InvokeMethod { TargetType = typeof(ApplicationInterface), // 3. برای به روزرسانی لیست کاربران ثبت شده در نمایش فرم MethodName = "NewUser", Parameters = { new InArgument<User>(env => User.Get(env)) } }, new WaitForAccept<User> // 4. اینجا فرایند متوقف میشود و منتظر تایید مدیر میماند { BookmarkName = "GetAcceptes", Input = new OutArgument<User>(env => User.Get(env)) }, new WriteLine // 5. لاگ مربوط به تایید شدن کاربر { TextWriter = new InArgument<TextWriter>(c=> Writer.Get(c)), Text = new InArgument<string>(c=> string.Format("User {0} Accepter by {1}",Name.Get(c),User.Get(c).AcceptedBy)) } } }; } } }
اگر بخوایم از Designer استفاده کنیم. فرایندمان چیزی شبیه شکل زیر خواهد بود
به Application بر میگردیم تا آن را پیاده سازی کنیم. ابتدا به app.config که اتوماتیک ایجاد شده رفته تا اسم Connectionstring رو به UserGenerator تغییر دهیم. محتویات درون app.config به شکل زیر است.
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> </configSections> <connectionStrings> <add name="UserGenerator" connectionString="Data Source=.;Initial Catalog=PersistWF;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> </configuration>
در کلاس AddRequest کد زیر را اضافه میکنم. برای نگهداری مقدار connectionstring
private string _connectionString = "";
همچنین کدهای زیر را به رویداد Load فرم اضافه میکنم تا مقدار ConnectionString را از Config بخوانم:
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); ConnectionStringsSection css = (ConnectionStringsSection)config.GetSection("connectionStrings"); _connectionString = css.ConnectionStrings["UserGenerator"].ConnectionString;
خط زیر را هم به کلاس AddRequest اضافه نمایید.
private InstanceStore _instanceStore;
این ارجاعیه به کلاس InstanceStore که برای Persist و Load کردن workflow از آن استفاده میکنیم و کدهای زیر را هم به رویداد Load فرم اضافه میکنیم.
_instanceStore = new SqlWorkflowInstanceStore(_connectionString); InstanceView view = _instanceStore.Execute(_instanceStore.CreateInstanceHandle(), new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30)); _instanceStore.DefaultInstanceOwner = view.InstanceOwner;
InstanceStore یک کلاس abstract می باشد که همهی Providerهای مربوط به persistence از آن مشتق میشوند. در این پروژه من از کلاس SqlWorkflowInstanceStore استفاده کردم تا workflowها را در دیتابیسSQL Server ذخیره کنم.
برای ایجاد یک Request مقادیر را از فرم دریافت کرده، یک User ایجاد میکنیم و آن را در فرآیند به جریان میاندازیم. این کار را در رویداد کلیک دکمه Register انجام میدهیم
private void brnRegister_Click(object sender, RoutedEventArgs e) { Dictionary<string, object> parameters = new Dictionary<string, object>(); parameters.Add("Name", txtName.Text); parameters.Add("Phone", txtPhone.Text); parameters.Add("Email", txtEmail.Text); parameters.Add("ConnectionString", _connectionString); parameters.Add("Writer", new ListBoxTextWriter(lstEvents)); WorkflowApplication i = new WorkflowApplication (new UserWF(), parameters); // Setup persistence i.InstanceStore = _instanceStore; i.PersistableIdle = (waiea) => PersistableIdleAction.Unload; i.Run(); }
پارامترهای ورودی را از روی فرم مقدار دهی میکنیم. یک شی از کلاس WorkflowApplication ایجاد میکنیم. خاصیت InstanceStore آن را با Store ای که ایجاد کردیم مقدار دهی میکنیم. توسط رویداد PersistableIdle فرآیند رو مجبور میکنیم به Persist شدن و Unload شدن.
و سپس فرایند را اجرا میکنم.
اگر یادتان باشد، در فرآیند، از یک InvoceMethod استفاده کردیم. متد مورد نظر را هم در کلاس ApplicationInterface.cs ایجاد میکنیم.
public static void NewUser(User l) { if (_app != null) _app.AddNewUser(l); }
همین طور که میبینید، یک متد هم در کلاس AddRequest ایجاد میشود؛ با این محتوا
public void AddNewUser(User l) { this.lstUsers.Dispatcher.BeginInvoke(new Action(() => this.lstUsers.Items.Add(l))); }
این متد فقط یک کاربر را به لیست کاربران اضافه میکند. این لیست همه کاربران را نشان میدهد. توسط رویداد SelectionChanged این کنترل، کاربر انتخاب شده را بررسی کرده در صورتی که کاربر جدید باشد، امکان تایید شدن را برایش فراهم میکنیم؛ که نمایش دکمه تایید است.
private void lstUsers_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (lstUsers.SelectedIndex >= 0) { User l = (User)lstUsers.Items[lstUsers.SelectedIndex]; lblSelectedNotes.Visibility = Visibility.Visible; if (l.Status == "New") { lblAgent.Visibility = Visibility.Visible; txtAcceptedBy.Visibility = Visibility.Visible; btnAccept.Visibility = Visibility.Visible; } else { lblAgent.Visibility = Visibility.Hidden; txtAcceptedBy.Visibility = Visibility.Hidden; btnAccept.Visibility = Visibility.Hidden; } } else { lblSelectedNotes.Content = ""; lblSelectedNotes.Visibility = Visibility.Hidden; lblAgent.Visibility = Visibility.Hidden; txtAcceptedBy.Visibility = Visibility.Hidden; btnAccept.Visibility = Visibility.Hidden; } }
و برای رویداد کلیک دکمه تایید کاربر :
private void btnAccept_Click(object sender, RoutedEventArgs e) { if (lstUsers.SelectedIndex >= 0) { User u = (User)lstUsers.Items[lstUsers.SelectedIndex]; Guid id = u.WorkflowID.Value; UserDataDataContext dc = new UserDataDataContext(_connectionString); dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Users); u = dc.Users.SingleOrDefault<User>(x => x.WorkflowID == id); if (u != null) { u.AcceptedBy = txtAcceptedBy.Text; u.Status = "Assigned"; dc.SubmitChanges(); // Clear the input txtAcceptedBy.Text = ""; } // Update the grid lstUsers.Items[lstUsers.SelectedIndex] = u; lstUsers.Items.Refresh(); WorkflowApplication i = new WorkflowApplication(new UserWF()); i.InstanceStore = _instanceStore; i.PersistableIdle = (waiea) => PersistableIdleAction.Unload; i.Load(id); try { i.ResumeBookmark("GetAcceptes", u); } catch (Exception e2) { AddEvent(e2.Message); } } }
کاربر را انتخاب میکنم مقادیرش را تنظیم میکنیم. آن را دخیره کرده و workflow را از روی guid مربوط به آن که قبلا در فرآیند به Entity دادیم، Load میکنیم و همانطور که میبینید توسط متد ResumeBookmark فرآیند رو از جایی که میخواهیم ادامه میدهیم. البته میتوان تایید کاربر را هم در خود فرآیند انجام داد و چون نوشتن Activity مرتبط با آن تقریبا تکراری است با اجازهی شما من اون رو ننوشتم و زحمتش با خودتونه.
حالا فقط ماندهاست که همه کاربران را در ابتدای نمایش فرم از دیتابیس فراخوانی کنیم و در لیست نمایش دهیم:
private void LoadExistingLeads() { UserDataDataContext dc = new UserDataDataContext(_connectionString); dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Users); IEnumerable<User> q = dc.Users; foreach (User u in q) { AddNewUser(u); } }
و فراخوانی این متد را به انتهای رویداد Load صفحه واگذار میکنیم.
پروژه رو اجرا کرده و یک کاربر را اضافه میکنم. همانطور که میدانید این کاربر در فرآیند ایجاد و در دیتابیس ذخیره میشود
برنامه را میبندم و دوباره اجرا میکنم. کاربر را انتخاب میکنم و یک نام برای admin انتخاب و آن را تایید میکنم. فرآیند را از bookmark مورد نظر اجرا کرده و به پایان میرسد. با بسته شدن برنامه، فرایند Idle و Unload میشود و ذخیره آن در sqlserver صورت میگیرد.
GET /index.html HTTP/1.1 Host: www.dotnettips.info |
GET https://www.dntips.ir/ HTTP/1.1 Host: www.dotnettips.info Connection: keep-alive Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17 Accept-Encoding: gzip,deflate,sdch Accept-Language: en-US,en;q=0.8 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3 Cookie: BlogPost-1175=NLOpR%2fgHcUGqPL8dZYv3BDDqgd4xOtiiNxHIp1rD%2bAQ%3d; BlogComment-5002=WlS1iaIsiBnQN1UDD4p%2fHFvuoxC3b8ckbw78mAWXZOSWMpxPlLo65%2bA40%2flFVR54; ReaderEmail=DP%2bx4TEtMT2LyhNQ5QqsArka%2fWALP5LYX8Y
HTTP/1.1 200 OK Content-type: text/html Set-Cookie: cookieName=cookieValue Set-Cookie: cookieName2=cookieValue2; Expires=Thr, 10-Jun-2021 10:18:14 GMT ... |
HTTP/1.1 200 OK Date: Wed, 30 Jan 2013 20:25:15 GMT Server: Microsoft-IIS/6.0 X-Powered-By: ASP.NET X-Compressed-By: HttpCompress Set-Cookie: .ASPXROLES=NzZ9qIRpCWHofryYglbsQFv_SSGPn7ivo0zKFoS94gcODVdIKQAe_IBwuc-TQ-03jGeIkZabTuxA0A3k2-nChy7iAWw9rPMYXSkqzMkizRFkDC0k3gQTkdLqLmmeIfnL9UjfMNWO8iVkYQrSv24ecbpFDSQCH827V2kEj8k2oCm_5sKRSmFpifh4N7kinEi0vomG1vW4Rbg9JWMhCgcvndvsFsXxpj-NiEikC1RqHpiLArIyalEMEN-cIuVtRe7uoo938u9l-7OXb8yzXucVl4bdqPy2DXM3ddWzb3OH1jSFM6gxwJ8qRZDlSGmEEbhji7rA-efI4aYGTKx6heWfUsY6E2k73jJLbuZ3RB4oNwRYmz8FRB0-vm1pO7rhF1JIoi1YB17ez-Ox5chNEFkPVREanHVU9DxboJ5dKgN-2B5udUFPunnshbN8EBhixbFQOpqRiiOK4uWWaWy3rVEJYpCCDBRctKCfEyYD1URFYeajB0AXmiMUTcGeuUtwb-XFjbQZnbylmMF3EJgG16bcc1IEkTAUv1JfKjaql0XGWJI1; path=/; HttpOnly Cache-Control: private Content-Type: text/html; charset=utf-8 Content-Length: 106727 <!DOCTYPE html> <html> ...
GET /index2.html HTTP/1.1 Host: www.example.org Cookie: cookieName=cookieValue; cookieName2=cookieValue2 Accept: */* |
در قسمت قبل به معرفی postgresql پرداختیم; در این قسمت قصد ایجاد و راه اندازی یک api با استفاده از دیتابیس postgresql و استفاده از تکنولوژیهای آن را با استفاده از docker داریم.
ابتدا با استفاده از دستور زیر یک پروژهی جدید asp.net core را ایجاد کنید:
dotnet new webapi --minimal -o YourDirectoryPath:\YourFolderName
سپس فایل docker-compose.yaml را به روت پروژه اضافه کنید که شامل کانفیگهای زیر میباشد:
version: '3.1' services: db: image: postgres container_name: db restart: always environment: POSTGRES_PASSWORD: postgres POSTGRES_USERNAME: postgres POSTGRES_DB: BloggingDb ports: - "5432:5432" volumes: - postgres_data:/data/db adminer: image: adminer restart: always ports: - 8080:8080 pgadmin4: image: dpage/pgadmin4 restart: always environment: PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org PGADMIN_DEFAULT_PASSWORD: admin PGADMIN_CONFIG_SERVER_MODE: 'False' ports: - 5050:80 volumes: - pgadmin:/var/lib/pgadmin depends_on: - db volumes: postgres_data: pgadmin:
سپس با اجرای دستور زیر در روت پروژه، سرویسها را راه اندازی کنید:
docker compose up -d
معرفی سرویسهای استفاده شده در تنظیمات فایل بالا:
سرویس db :
نمونه ایمیج اصلی، volume، تنظیمات connection string در آن استفاده شده است.
سرویس adminer :
https://hub.docker.com/_/adminer /
Adminer - Database management in a single PHP file
یک برنامه تحت وب مدیریت پایگاه داده ساده میباشد که ویژگیها MySql را در کنار سرعت و امنیت ارائه میدهد و در آدرس http://localhost:8080 / اجرا خواهد شد.
سرویس pgadmin4 :
dpage/pgadmin4 - Docker Image | Docker Hub
در حال حاضر این برنامه محبوبترین برنامه مدیریت پایگاه داده میباشد که ویژگیهای پیشرفتهای را نیز پوشش میدهد و در آدرس http://localhost:5050 / اجرا خواهد شد.
اکنون نوبت نوشتن کدها میباشد.
- تنظیم connection string در فایل appsettings.json:
"ConnectionStrings": { "BloggingContext": "Username=postgres;Password=postgres;Server=localhost;Database=BloggingDb” }
- و همینطور پکیجهای زیر را به برنامه خود رفرنس دهید:
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL dotnet add package Microsoft.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.Design
- مدلهای برنامه را در مسیر /Models ایجاد کنید:
namespace NpgsqlAPI.Models; public class Post { public int PostId { get; set; } public string Title { get; set; } = null!; public string Content { get; set; } = null!; public int BlogId { get; set; } public Blog Blog { get; set; } = null!; } namespace NpgsqlAPI.Models; public class Blog { public int BlogId { get; set; } public string? Url { get; set; } public List<Post>? Posts { get; set; } }
- سپس BloggingContext را در مسیر /Data ایجاد کنید:
using Microsoft.EntityFrameworkCore; using NpgsqlAPI.Models; namespace NpgsqlAPI.Data; public class BloggingContext : DbContext { public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { } public DbSet<Blog> Blogs => Set<Blog>(); public DbSet<Post
- سپس اینترفیس IBlogServices را در مسیر /Servicec/Blogs ایجاد کنید:
using NpgsqlAPI.Models; namespace NpgsqlAPI.Services.Blogs; public interface IBlogServices { Task<IEnumerable<Blog>> GetList(); Task<Blog?> Get(uint id); Task<uint> Add(Blog obj); Task AddRange(Blog[] obj); Task Update(Blog obj); Task UpdateRange(Blog[] obj); Task Remove(uint id); }
- و سپس پیاده سازی آن را در فایل BlogEFServices و در کنار اینترفیس آن قرار دهید:
using Microsoft.EntityFrameworkCore; using NpgsqlAPI.Data; using NpgsqlAPI.Models; namespace NpgsqlAPI.Services.Blogs; public sealed class BlogEFServices : IBlogServices { private readonly BloggingContext _context; public BlogEFServices(BloggingContext context) { _context = context; } public async Task<uint> Add(Blog obj) { await _context.Blogs.AddAsync(obj); return (uint)await SaveChangesAsync(); } public async Task AddRange(Blog[] obj) { await _context.Blogs.AddRangeAsync(obj); await SaveChangesAsync(); } public async Task<Blog?> Get(uint id) { return await _context.Blogs.FirstOrDefaultAsync(x=>x.BlogId == id); } public async Task<IEnumerable<Blog>> GetList() { return await _context.Blogs.ToListAsync(); } public async Task Remove(uint id) { var entity = await Get(id); _context.Blogs.Remove(entity!); await SaveChangesAsync(); } public async Task Update(Blog obj) { _context.Blogs.Update(obj); await SaveChangesAsync(); } public async Task UpdateRange(Blog[] obj) { _context.Blogs.UpdateRange(obj); await SaveChangesAsync(); } private async Task<int> SaveChangesAsync() { return await _context.SaveChangesAsync(); } }
- اکنون endpointهای api را در فایل program.cs ایجاد کنید:
using System.Data; using Microsoft.EntityFrameworkCore; using Npgsql; using NpgsqlAPI.Services.Blogs; using NpgsqlAPI.Data; using NpgsqlAPI.Models; var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); string connectionString = builder.Configuration.GetConnectionString("BloggingContext")!; builder.Services.AddDbContext<BloggingContext>(options => options.UseNpgsql(connectionString)); builder.Services.AddTransient<IDbConnection>(_ => new NpgsqlConnection(connectionString)); // builder.Services.AddScoped<IBlogServices, BlogDapperServices>(); // builder.Services.AddScoped<IBlogServices, BlogEFRawQueryServices>(); builder.Services.AddScoped<IBlogServices, BlogEFServices>(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.MapGet("/blogs", async (IBlogServices service) => await service.GetList()) .WithName("GetBlogs") .WithOpenApi(); app.MapGet("/blogs/{id}", async (IBlogServices service, uint id) => await service.Get(id)) .WithName("GetBlog") .WithOpenApi(); app.MapPost("/blogs", async (IBlogServices service, Blog blog) => await service.Add(blog)) .WithName("AddBlog") .WithOpenApi(); app.MapDelete("/blogs/{id}", async (IBlogServices service, uint id) => await service.Remove(id)) .WithName("RemoveBlog") .WithOpenApi(); app.MapPut("/blogs", async (IBlogServices service, Blog blog) => await service.Update(blog)) .WithName("UpdateBlog") .WithOpenApi(); app.MapPut("/blogs/Bulk", async (IBlogServices service, Blog[] blogs) => await service.UpdateRange(blogs)) .WithName("UpdateBulkBlog") .WithOpenApi(); app.MapPost("/blogs/Bulk", async (IBlogServices service, Blog[] blogs) => await service.AddRange(blogs)) .WithName("AddBulkBlog") .WithOpenApi(); app.Run();
تمامی کدهای برنامه تا به اینجا نوشته شدهاند. اکنون migration را پس از اطمینان از اجرا بودن داکر اجرا کنید
dotnet ef migrations add Init dotnet ef database update
و برنامه را اجرا و تست کنید.
class Counter extends Component { state = { count: 0, tags: ["tag 1", "tag 2", "tag 3"] };
class Counter extends Component { state = { count: 0, tags: ["tag 1", "tag 2", "tag 3"] }; render() { return ( <div> <span className={this.getBadgeClasses()}>{this.formatCount()}</span> <button className="btn btn-secondary btn-sm">Increment</button> <ul> {this.state.tags.map(tag => ( <li>{tag}</li> ))} </ul> </div> ); }
<li key={tag}>{tag}</li>
renderTags() { if (this.state.tags.length === 0) { return <p>There are no tags!</p>; } return ( <ul> {this.state.tags.map(tag => ( <li key={tag}>{tag}</li> ))} </ul> ); }
render() { return ( <div> <span className={this.getBadgeClasses()}>{this.formatCount()}</span> <button className="btn btn-secondary btn-sm">Increment</button> {this.renderTags()} </div> ); }
state = { count: 0, tags: [] };
{this.state.tags.length === 0 && "Please create a new tag!"}
handleIncrement() { console.log("Increment clicked!"); }
<button onClick={this.handleIncrement} className="btn btn-secondary btn-sm" > Increment </button>
handleIncrement() { console.log("Increment clicked!", this.state.count); }
constructor() { super(); console.log("constructor", this); this.handleIncrement = this.handleIncrement.bind(this); }
handleIncrement = () => { console.log("Increment clicked!", this.state.count); }
handleIncrement = () => { this.state.count++; };
Line 33:5: Do not mutate state directly. Use setState() react/no-direct-mutation-state
handleIncrement = () => { this.setState({ count: this.state.count + 1 }); };
doHandleIncrement = () => { this.handleIncrement({ id: 1, name: "Product 1" }); };
handleIncrement = product => { console.log(product); this.setState({ count: this.state.count + 1 }); };
onClick={() => this.handleIncrement({ id: 1, name: "Product 1" })}
x.For<IMyInterface>().HybridHttpOrThreadLocalScoped().Use<MyClass>();