- 投稿日:2020-06-28T22:18:59+09:00
ステップ関数
private void button_Click(object sender, EventArgs e) { var x = np_arange(-5.0, 5.0, 0.1); var y = step_function(x); var plt = new Chart(); plt_plot(plt, x, y); plt_xlim(plt, -6.0, 6.0); plt_ylim(plt, -0.1, 1.1); plt_show(plt); } private U[] np_arange<T, U>(T start, T end, U step) where T : struct where U : struct { var start_ = (dynamic)start; var end_ = (dynamic)end; var step_ = (dynamic)step; return Enumerable.Range((int)(start_ / step_), (int)((end_ - start_) / step_)).Select(x => (U)(x * step_)).ToArray(); } private int[] step_function<T>(T[] x) where T : struct { return x.Select(n => ((dynamic)n > 0) ? 1 : 0).ToArray(); } private void plt_plot<T, U>(Chart plt, T[] x, U[] y) where T : struct where U : struct { plt.Size = new System.Drawing.Size(500, 350); plt.ChartAreas.Add("step"); plt.Legends.Add("step"); plt.Series.Add("step"); plt.ChartAreas["step"].AxisX.MajorGrid.Enabled = false; plt.ChartAreas["step"].AxisX.LabelStyle.Format = "0.0"; plt.ChartAreas["step"].AxisX.Minimum = (dynamic)x.First(); plt.ChartAreas["step"].AxisX.Maximum = (dynamic)x.Last(); plt.ChartAreas["step"].AxisY.MajorGrid.Enabled = false; plt.ChartAreas["step"].AxisY.LabelStyle.Format = "0.0"; plt.ChartAreas["step"].AxisY.Minimum = (dynamic)y.First(); plt.ChartAreas["step"].AxisY.Maximum = (dynamic)y.Last(); plt.Series["step"].ChartType = SeriesChartType.Line; foreach (var n in x.Zip(y, Tuple.Create)) { plt.Series["step"].Points.AddXY(n.Item1, n.Item2); } } private void plt_xlim(Chart plt, double minimum, double maximum) { plt.ChartAreas["step"].AxisX.IntervalOffset = 0.0; plt.ChartAreas["step"].AxisX.Minimum = minimum; plt.ChartAreas["step"].AxisX.Maximum = maximum; plt.ChartAreas["step"].AxisX.Interval = 2; } private void plt_ylim(Chart plt, double minimum, double maximum) { plt.ChartAreas["step"].AxisY.IntervalOffset = 0.1; plt.ChartAreas["step"].AxisY.Minimum = minimum; plt.ChartAreas["step"].AxisY.Maximum = maximum; plt.ChartAreas["step"].AxisY.Interval = 0.2; } private void plt_show(Chart plt) { Bitmap bitmap = new Bitmap(plt.Width, plt.Height); plt.DrawToBitmap(bitmap, new Rectangle(System.Drawing.Point.Empty, plt.Size)); Cv2.ImShow("plot", BitmapConverter.ToMat(bitmap)); }参考文献:
斎藤康毅(2016)『ゼロから作るDeep Learning』オライリー・ジャパン
- 投稿日:2020-06-28T20:08:48+09:00
[Microsoft] 3. 機能コンポーネントを作成する - Angularチュートリアル Tour of Heroes を Blazor で再実装する
機能コンポーネントを作成する
作成したHeroesコンポーネントを、一覧部分と詳細部分に分けます。
HeroDetailコンポーネントを作成する
dotnet cliで作成します。
dotnet new razorcomponent --output BlazorTourOfHeroes/Shared --name HeroDetail
テンプレート部分を作成する
HeroDetailコンポーネントのテンプレート部分を作成します。
Shared/HeroDetail.razor@if (Hero != null) { <h2>@Hero.Name.ToUpper() Details</h2> <div><span>Id: </span>@Hero.Id</div> <div> <label>Name: <input @bind="Hero.Name" placeholder="name"/> </label> </div> }
[Parameter]
アノテーションをつける外部からHeroオブジェクトを受け取るために、
[Parameter]
アノテーションをつけたHero
プロパティを作成します。Shared/HeroDetail.razor[Parameter] public Hero Hero { get; set; }HeroDetailコンポーネントを表示する
Heroesコンポーネントにある詳細表示部分をHeroDetailコンポーネントに置き換えます。
Heroコンポーネントを変更する
詳細表示部分をHeroDetailコンポーネントに置き換えます。
Shared/Heroes.razor<HeroDetail Hero="selectedHero"></HeroDetail>テンプレート全体はこのようになります。
Shared/Heroes.razor<h2>My Heroes</h2> <ul class="heroes"> @foreach (var hero in heroes) { <li @onclick="@(_ => OnSelect(hero))" class="@(selectedHero == hero ? "selected" : "")" ><span class="badge">@hero.Id</span> @hero.Name </li> } </ul> <HeroDetail Hero="selectedHero"></HeroDetail>なにが変わった?
一覧表示をHeroesコンポーネント、詳細表示をHeroDetailコンポーネントとして分けたことで、メンテナンス性が良くなりました。
こんなんできました
Shared/Heroes.razor@using BlazorTourOfHeroes.Model <h2>My Heroes</h2> <ul class="heroes"> @foreach (var hero in heroes) { <li @onclick="@(_ => OnSelect(hero))" class="@(selectedHero == hero ? "selected" : "")" ><span class="badge">@hero.Id</span> @hero.Name </li> } </ul> <HeroDetail Hero="selectedHero"></HeroDetail> @code { private List<Hero> heroes = MockHeroes.Create(); private Hero selectedHero = null; private void OnSelect(Hero hero) { selectedHero = hero; } }Shared/HeroDetail.razor@using BlazorTourOfHeroes.Model @if (Hero != null) { <h2>@Hero.Name.ToUpper() Details</h2> <div><span>Id: </span>@Hero.Id</div> <div> <label>Name: <input @bind="Hero.Name" placeholder="name"/> </label> </div> } @code { [Parameter] public Hero Hero { get; set; } }
- 投稿日:2020-06-28T18:15:01+09:00
【Azure Functions】Qiita APIで取得したデータをSQL Databaseに保存する
はじめに
この記事では、Qiita APIから自分が投稿した記事のView数を取得し、DBに保存するということをやっていきます。実現方法としては、Azure FunctionsのTimmer Trigger関数を使って、定期的に情報を取得し、Azure SQL Databaseに保存します。また、DBへの接続文字列などの情報はAzure KeyVaultに保存し、プログラムから参照するような構成とします。
開発環境
ローカルマシン(Mac:MacOS Catalina v10.15.4)で開発したものをAzureへデプロイする形で開発を行います。VS Codeの拡張機能であるAzure Functions for Visual Studio Codeを使用します。
Azure SQL Databaseの作成
はじめにデータを保存するためのSQL Databaseを作成しておきます。詳細な手順は省略しますがMicrosoft公式ドキュメントを参考に作成してください。できるだけお金がかからないように最小スペックで作成します。
- SQL Databaseのスペック(一部抜粋)
項目 値 価格レベル Basic ストレージ容量 2GB Azure Functionsにデプロイするプログラムの作成
Microsoft公式ドキュメント(クイック スタート:Visual Studio Code を使用して Azure で関数を作成する)の通りにローカル環境にプロジェクトを作成します。テンプレート選択のところはTimer Triggerを選択してください。今回はC#を使用して開発していきます。また、この記事では順を追ってプログラムを作成していきますが最終的なプログラムはGitHubで公開しています。
Qiita APIで記事のView数を取得する
テンプレートを生成できたところで、まずはQiita APIで記事のView数を取得する処理を書いていきます。テンプレートのファイルに指定したURIにAPIリクエストを送る関数GetJsonを定義します。指定するURIはQiitaの公式ドキュメントを参考に決定します。今回は自分が投稿した記事の一覧を取得するAPIを使用します。
- 自分が投稿した記事の一覧を取得するAPI
https://qiita.com/api/v2/users/[Qiitaのユーザー名]/items
しかし、このAPIをただ使用するだけでは情報は取得できません。一般に公開されていない情報(ユーザーに関する情報や記事のview数など)はアクセストークを付与したリクエストを送る必要があります。なのでQiita APIで情報を取得するために必要なアクセストークンを格納するクラスも別ファイルとして作成しておきます。アクセストークンを発行していない場合はユーザー設定から発行しておきます。
Qiita APIで記事の情報を取得するプログラム
get_qiita_views.csusing System; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace kanazawa.Function { public static class get_qiita_views { [FunctionName("get_qiita_views")] public static async void Run([TimerTrigger("0 0 * * * *")]TimerInfo myTimer, ILogger log) { TimeZoneInfo jstTimeZone = TZConvert.GetTimeZoneInfo("Tokyo Standard Time"); DateTime utcTime = DateTime.UtcNow; DateTime jstTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, jstTimeZone); log.LogInformation($"C# Timer trigger function executed at: {jstTime}"); // Qiita APIのURL string url = "https://qiita.com/api/v2/users/" + Parameter.getQiitaUserName() + "/items"; // 投稿記事情報取得 string json = await GetJson(url); } private static async Task<string> GetJson(string url) { var httpClient = new System.Net.Http.HttpClient(); // OAuth 2.0 Authorization Headerの設定 httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Parameter.getQiitaAccessToken()); var request = new HttpRequestMessage(HttpMethod.Get, url); HttpResponseMessage response = await httpClient.SendAsync(request); string result = await response.Content.ReadAsStringAsync(); return result; } } }Parameter.csusing System; namespace kanazawa.Function { public class Parameter { public static string getQiitaAccessToken(){ return "******"; } public static string getQiitaUserName(){ return "******"; } } }取得結果はJson形式なので、これをデシリアライズ(C#のオブジェクトに変換)する必要があります。デシリアライズするためにはデータを格納するモデルクラスが必要となりますが、手動で作成するのはかなり面倒です。そのため以下のサイトで自動でモデルクラスを作成してもらいます。
curlコマンド等で別途Jsonを取得し、上記サイトでモデルクラスを作成しましょう。ここで1点注意点があります。上記サイトで生成されたモデルクラスには一部問題があり、私の場合は余計なフィールドがenum型で宣言されていました。この後実際にデシリアライズする際にエラーが出るので、その時でも良いですが
自分で確認して修正しましょう。
Jsonをデシリアライズする際に使用するモデルクラス
QiitaInformationModel.cs// <auto-generated /> // // To parse this JSON data, add NuGet 'Newtonsoft.Json' then do: // // using kanazawa.Function; // // var qiitaInformation = QiitaInformation.FromJson(jsonString); using System; using System.Collections.Generic; using System.Globalization; using Newtonsoft.Json; using Newtonsoft.Json.Converters; namespace kanazawa.Function { public partial class QiitaInformationModel { [JsonProperty("rendered_body")] public string RenderedBody { get; set; } [JsonProperty("body")] public string Body { get; set; } [JsonProperty("coediting")] public bool Coediting { get; set; } [JsonProperty("comments_count")] public long CommentsCount { get; set; } [JsonProperty("created_at")] public DateTimeOffset CreatedAt { get; set; } [JsonProperty("group")] public object Group { get; set; } [JsonProperty("id")] public string Id { get; set; } [JsonProperty("likes_count")] public long LikesCount { get; set; } [JsonProperty("private")] public bool Private { get; set; } [JsonProperty("reactions_count")] public long ReactionsCount { get; set; } [JsonProperty("tags")] public Tag[] Tags { get; set; } [JsonProperty("title")] public string Title { get; set; } [JsonProperty("updated_at")] public DateTimeOffset UpdatedAt { get; set; } [JsonProperty("url")] public Uri Url { get; set; } [JsonProperty("user")] public User User { get; set; } [JsonProperty("page_views_count")] public int PageViewsCount { get; set; } } public partial class Tag { [JsonProperty("name")] public string Name { get; set; } [JsonProperty("versions")] public object[] Versions { get; set; } } public partial class User { [JsonProperty("description")] public string Description { get; set; } [JsonProperty("facebook_id")] public string FacebookId { get; set; } [JsonProperty("followees_count")] public long FolloweesCount { get; set; } [JsonProperty("followers_count")] public long FollowersCount { get; set; } [JsonProperty("github_login_name")] public string GithubLoginName { get; set; } [JsonProperty("id")] public string Id { get; set; } [JsonProperty("items_count")] public long ItemsCount { get; set; } [JsonProperty("linkedin_id")] public string LinkedinId { get; set; } [JsonProperty("location")] public string Location { get; set; } [JsonProperty("name")] public string Name { get; set; } [JsonProperty("organization")] public string Organization { get; set; } [JsonProperty("permanent_id")] public long PermanentId { get; set; } [JsonProperty("profile_image_url")] public Uri ProfileImageUrl { get; set; } [JsonProperty("team_only")] public bool TeamOnly { get; set; } [JsonProperty("twitter_screen_name")] public object TwitterScreenName { get; set; } [JsonProperty("website_url")] public string WebsiteUrl { get; set; } } public partial class QiitaInformation { public static QiitaInformation[] FromJson(string json) => JsonConvert.DeserializeObject<QiitaInformation[]>(json, kanazawa.Function.Converter.Settings); } public static class Serialize { public static string ToJson(this QiitaInformation[] self) => JsonConvert.SerializeObject(self, kanazawa.Function.Converter.Settings); } internal static class Converter { public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore, DateParseHandling = DateParseHandling.None, Converters = { new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } }, }; } }モデルクラスが完成したらデシリアライズの処理を追記していきます。また、記事のview数は記事の一覧取得のAPIからは取得できないので、記事ごとの詳細を取得するAPIを発行する処理も追記します。
- 自分が投稿した記事ごとの詳細を取得するAPI
https://qiita.com/api/v2/items/[記事のID]
デシリアライズとView数取得処理を追記
get_qiita_views.csusing System; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace kanazawa.Function { public static class get_qiita_views { [FunctionName("get_qiita_views")] public static async void Run([TimerTrigger("0 0 * * * *")]TimerInfo myTimer, ILogger log) { log.LogInformation($"C# Timer trigger function executed at: {jstTime}"); // Qiita APIのURL string url = "https://qiita.com/api/v2/users/" + Parameter.getQiitaUserName() + "/items"; // 投稿記事情報取得 string json = await GetJson(url); // デシリアライズ時の設定 var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore }; // デシリアライズ List<QiitaInformationModel> models = JsonConvert.DeserializeObject<List<QiitaInformationModel>>(json, settings); // 各投稿記事のView数を取得 string getViewsCountUrl; foreach (var model in models) { getViewsCountUrl = "https://qiita.com/api/v2/items/" + model.Id; model.PageViewsCount = JsonConvert.DeserializeObject<QiitaInformationModel>(await GetJson(getViewsCountUrl)).PageViewsCount; log.LogInformation($"title: {model.Title}"); log.LogInformation($"views: {model.PageViewsCount}"); } } private static async Task<string> GetJson(string url) { var httpClient = new System.Net.Http.HttpClient(); // OAuth 2.0 Authorization Headerの設定 httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Parameter.getQiitaAccessToken()); var request = new HttpRequestMessage(HttpMethod.Get, url); HttpResponseMessage response = await httpClient.SendAsync(request); string result = await response.Content.ReadAsStringAsync(); return result; } } }取得したデータをAzure SQL Databaseに保存する
最初に作成したAzure SQL Databaseにデータを保存します。今回は予め以下のテーブルをDBに作成しておきました。
- qiita_items
記事の情報を格納するマスタテーブル
カラム名 型 id varchar(50) title varchar(100) created_at datetime
- page_views_count
記事の時間ごとのview数を格納するトランザクションテーブル
カラム名 型 id varchar(50) counted_at varchar(50) page_views_count int(4) アクセストークンを格納したクラスにDBへの接続文字列を格納します。また、メインのクラスにDBへの接続・保存処理も記述していきます。
DBへの接続・保存処理を追記
get_qiita_views.csusing System; using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace kanazawa.Function { public static class get_qiita_views { [FunctionName("get_qiita_views")] public static async void Run([TimerTrigger("0 0 * * * *")]TimerInfo myTimer, ILogger log) { TimeZoneInfo jstTimeZone = TZConvert.GetTimeZoneInfo("Tokyo Standard Time"); DateTime utcTime = DateTime.UtcNow; DateTime jstTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, jstTimeZone); log.LogInformation($"C# Timer trigger function executed at: {jstTime}"); // Qiita APIのURL string url = "https://qiita.com/api/v2/users/" + Parameter.getQiitaUserName() + "/items"; // 投稿記事情報取得 string json = await GetJson(url); // デシリアライズ時の設定 var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore }; // デシリアライズ List<QiitaInformationModel> models = JsonConvert.DeserializeObject<List<QiitaInformationModel>>(json, settings); // 各投稿記事のView数を取得 string getViewsCountUrl; foreach (var model in models) { getViewsCountUrl = "https://qiita.com/api/v2/items/" + model.Id; model.PageViewsCount = JsonConvert.DeserializeObject<QiitaInformationModel>(await GetJson(getViewsCountUrl)).PageViewsCount; log.LogInformation($"title: {model.Title}"); log.LogInformation($"views: {model.PageViewsCount}"); } // DB接続文字列の取得 var connectionString = Parameter.getConnectionString(); // データ保存 using (var connection = new SqlConnection(connectionString)) { // データベースの接続開始 connection.Open(); try { // マスタテーブルの更新チェック Database.checkMasterData(models, log, connection); // データを保存 Database.saveData(models, jstTime, log, connection); } catch (Exception exception) { log.LogInformation(exception.Message); throw; } finally { // データベースの接続終了 connection.Close(); } } } private static async Task<string> GetJson(string url) { var httpClient = new System.Net.Http.HttpClient(); // OAuth 2.0 Authorization Headerの設定 httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Parameter.getQiitaAccessToken()); var request = new HttpRequestMessage(HttpMethod.Get, url); HttpResponseMessage response = await httpClient.SendAsync(request); string result = await response.Content.ReadAsStringAsync(); return result; } } }Database.csusing System; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Data.SqlClient; using System.Data; namespace kanazawa.Function { public class Database { // 新たに記事が投稿された場合はマスタテーブルを更新 public static void checkMasterData(List<QiitaInformationModel> models, ILogger log, SqlConnection connection) { using (var transaction = connection.BeginTransaction()) { try { using (var selectCommand = new SqlCommand() { Connection = connection, Transaction = transaction }) { // SQLの準備 selectCommand.CommandText = @"SELECT id FROM qiita_items"; // SQLの実行 var table = new DataTable(); var adapter = new SqlDataAdapter(selectCommand); adapter.Fill(table); // 存在フラグ bool flg = false; foreach (var model in models) { flg = false; for (int i = 0; i < table.Rows.Count; i++) { if (table.Rows[i]["id"].ToString().Equals(model.Id)) { flg = true; break; } } if (flg == false) { using (var insertCommand = new SqlCommand() { Connection = connection, Transaction = transaction }) { // SQLの準備 insertCommand.CommandText = @"INSERT INTO qiita_items VALUES (@ID, @TITLE, @CREATED_AT)"; insertCommand.Parameters.Add(new SqlParameter("@ID", model.Id)); insertCommand.Parameters.Add(new SqlParameter("@TITLE", model.Title)); insertCommand.Parameters.Add(new SqlParameter("@CREATED_AT", model.CreatedAt)); // SQLの実行 insertCommand.ExecuteNonQuery(); log.LogInformation($"succeeded to insert master data: {model.Title}"); } } } } // コミット transaction.Commit(); log.LogInformation("Committed"); } catch { // ロールバック transaction.Rollback(); log.LogInformation("Rollbacked"); throw; } } } // 各記事のview数を保存 public static void saveData(List<QiitaInformationModel> models, DateTime jstTime, ILogger log, SqlConnection connection) { using (var transaction = connection.BeginTransaction()) { try { foreach (var model in models) { using (var command = new SqlCommand() { Connection = connection, Transaction = transaction }) { // SQLの準備 command.CommandText = @"INSERT INTO page_views_count VALUES (@ID, @COUNTED_AT, @PAGE_VIEWS_COUNT)"; command.Parameters.Add(new SqlParameter("@ID", model.Id)); command.Parameters.Add(new SqlParameter("@COUNTED_AT", jstTime.ToString("yyyy/MM/dd HH"))); command.Parameters.Add(new SqlParameter("@PAGE_VIEWS_COUNT", model.PageViewsCount)); // SQLの実行 command.ExecuteNonQuery(); log.LogInformation($"succeeded to insert data: {model.Title}"); } } // コミット transaction.Commit(); log.LogInformation("Committed"); } catch { // ロールバック transaction.Rollback(); log.LogInformation("Rollbacked"); throw; } } } } }Parameter.csusing System; namespace kanazawa.Function { public class Parameter { public static string getQiitaAccessToken(){ return "******"; } public static string getQiitaUserName(){ return "******"; } public static string getConnectionString(){ return "******"; } } }ここで一度ローカルでテスト実行してみましょう。SQL Databaseの方で接続元IPアドレスを制限している場合は、ローカルPCからアクセスできるように設定した上でテスト実行します。うまくいったら一度Azureへデプロイしましょう。デプロイ時にAzure Functionsのリソースを作成できるので合わせて作成します。
Azure KeyVaultの利用
ここまでの実装でQiitaからデータを取得して、DBに保存することができます。しかし、DBへの接続情報などをソースコードの中に記述してしまっているため、セキュリティー的によろしくありません。ここではAzure KeyVaultにシークレットとして保存し、Azure Functionsから参照できるようにソースコードの改善とAzureの設定を入れていきます。
Azure KeyVaultの作成
Microsoft公式ドキュメント(チュートリアル:Linux VM と Python アプリを使用してシークレットを Azure Key Vault に格納する)を参考にAzure KeyVaultの作成とシークレットの格納を行います。今回は以下の3つのシークレットを格納します。
- Qiitaのアクセストークン
- Qiitaユーザー名
- SQL Databaseへの接続文字列
Azure KeyVaultの作成とシークレットの格納が完了したら、作成したAzure Functionsからシークレットを参照できるように権限を付与します。まずはAzure FunctionsのマネージドIDを有効化し、権限を付与する対象を作成します。作成後、Azure KeyVaultのアクセスポリシー設定画面からAzure FunctionsのマネージドIDに対してシークレットの取得権限を付与します。詳しいやり方はMicrosoft公式ドキュメント(App Service と Azure Functions の Key Vault 参照を使用する)を参照してください。
Azure Functionsの修正
参照先の設定が終わったので、Azure Functions側にシークレットを参照するように設定とソースコードの修正を入れていきます。Azure Functionsのアプリケーション設定を入れるとAzure Functionsの実行環境の環境変数にその値が反映されるのでソースコードから参照できるようになります。Microsoft公式ドキュメント(App Service と Azure Functions の Key Vault 参照を使用する)を参考にAzure Functionsに以下のアプリケーション設定を追加します。
- Qiitaのアクセストークン
- Qiitaユーザー名
- SQL Databaseへの接続文字列
上記ドキュメントにも記載されていますが、値にはAzure KeyVaultへの参照構文を入力します。参照構文は以下の形式です。
- @Microsoft.KeyVault(SecretUri=[参照したいシークレットのシークレット識別子])
シークレット識別子はAzure KeyVaultの該当シークレットの設定変更画面から取得できます。
ここまで準備ができたら後はソースコードを修正するだけです。各種秘匿情報を格納していたクラスを環境変数を参照するように修正します。
環境変数を参照するように修正
Parameter.csusing System; namespace kanazawa.Function { public class Parameter { public static string getQiitaAccessToken(){ return Environment.GetEnvironmentVariable("Qiita-Access-Token"); } public static string getQiitaUserName(){ return Environment.GetEnvironmentVariable("Qiita-User-Name"); } public static string getConnectionString(){ return Environment.GetEnvironmentVariable("ConnectionString"); } } }これでソースコード内に秘匿すべき情報を記述せずにQiitaからのデータ取得とDBへの保存ができるようになりました。実際にデプロイしてみて試してみましょう!今回の例では一時間に一回プログラムが実行されるようにスケジューーリングしているので、DBには一時間ごとの各記事のview数が保存されるはずです。実行のスケジューリングを変更したい場合はソースコードの以下の部分を変更して再デプロイしてください。
get_qiita_views.cs... public static class get_qiita_views { [FunctionName("get_qiita_views")] public static async void Run([TimerTrigger("0 0 * * * *")]TimerInfo myTimer, ILogger log) { ...TimerTrigger("0 0 * * * *")の引数で定義されているスケジューリング設定は左から秒、分、時間、日、月、曜日を表しています。*と記述することで毎秒、毎分といった意味となります。他にも表現方法はあるので興味のある方はMicrosoft公式ドキュメント(Azure Functions のタイマー トリガー)を参照してください。
- 投稿日:2020-06-28T17:31:36+09:00
[Microsoft] 2. リストを表示する - Angularチュートリアル Tour of Heroes を Blazor で再実装する
選択肢リストを表示する
前回からの続きです。
ヒーローの一覧を表示できるようにします。
一覧からヒーローを選択すると、詳細が表示されるようにします。
ヒーローたちのモックを作成する
ヒーローのリストを返すクラスを作成しました。
Model/MockHeroes.csusing System.Collections.Generic; namespace BlazorTourOfHeroes.Model { public class MockHeroes { public static List<Hero> Create() => new List<Hero> { new Hero { Id= 11, Name= "Dr Nice", }, new Hero { Id= 12, Name= "Narco" }, new Hero { Id= 13, Name= "Bombasto" }, new Hero { Id= 14, Name= "Celeritas" }, new Hero { Id= 15, Name= "Magneta" }, new Hero { Id= 16, Name= "RubberMan" }, new Hero { Id= 17, Name= "Dynama" }, new Hero { Id= 18, Name= "Dr IQ" }, new Hero { Id= 19, Name= "Magma" }, new Hero { Id= 20, Name= "Tornado" }, }; } }ヒーローたちを表示する
Heroesコンポーネントに
heroes
変数を追加します。Shared/Heroes.razorprivate List<Hero> heroes = MockHeroes.Create();ヒーローたちを一覧する
foreach
でヒーローたちをイテレートします。Shared/Heroes.razor<h2>My Heroes</h2> <ul class="heroes"> @foreach (var hero in heroes) { <li><span class="badge">@hero.Id</span> @hero.Name </li> } </ul>ヒーローたちにスタイルをあてる
全体共通のスタイルシートを編集します。
Anguarのチュートリアルからコピペしました。
こんなんできました
詳細表示を作成する
一覧をクリックしたら、詳細を表示するようにします。
クリックイベントをバインドする
<li>
タグにクリックイベントを追加します。引数を渡したいので、ラムダにします。
Shared/Heroes.razor<li @onclick="@(_ => OnSelect(hero))">クリックイベントハンドラを作成する
選択されたヒーローを格納する変数とイベントハンドラを作成します。
Shared/Heroes.razorprivate Hero selectedHero; private void OnSelect(Hero hero) { selectedHero = hero; }詳細表示部分を追加する
選択されたヒーローを表示するようにします。
Shared/Heroes.razor<h2>@selectedHero.Name.ToUpper() Details</h2> <div><span>Id: </span>@selectedHero.Id</div> <div> <label>Name: <input @bind="selectedHero.Name" placeholder="name"/> </label> </div>ここで実行すると、ブラウザのコンソールにぬるぽが出力されました。
crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: Object reference not set to an instance of an object. System.NullReferenceException: Object reference not set to an instance of an object. at BlazorTourOfHeroes.Shared.Heroes.BuildRenderTree (Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) <0x2c456d0 + 0x00246> in <filename unknown>:0 at Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0 (Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) <0x2b30640 + 0x0001a> in <filename unknown>:0 at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch (Microsoft.AspNetCore.Components.Rendering.RenderBatchBuilder batchBuilder, Microsoft.AspNetCore.Components.RenderFragment renderFragment) <0x2b2ff78 + 0x00054> in <filename unknown>:0 at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch (Microsoft.AspNetCore.Components.Rendering.RenderQueueEntry renderQueueEntry) <0x2b2da88 + 0x0004c> in <filename unknown>:0 at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue () <0x2b2d1c8 + 0x00092> in <filename unknown>:0 blazor.webassembly.js:formatted:2093ぬるぽを修正
selectedHero
がnullのときは、詳細部分を出力しないようにします。Shared/Heroes.razor@if (selectedHero != null) { <h2>@selectedHero.Name.ToUpper() Details</h2> <div><span>Id: </span>@selectedHero.Id</div> <div> <label>Name: <input @bind="selectedHero.Name" placeholder="name"/> </label> </div> }選択中のヒーローにスタイルをあてる
class
属性の中でRazor構文を使います。Shared/Heroes.razor<li @onclick="@(_ => OnSelect(hero))" class="@(selectedHero == hero ? "selected" : "")" ><span class="badge">@hero.Id</span> @hero.Name </li>こんなんできました
- 投稿日:2020-06-28T15:08:15+09:00
[Microsoft] 1. ヒーローエディタ - Angularチュートリアル Tour of Heroes を Blazor で再実装する
ヒーローエディタ
雛形が出来上がりましたので、ヒーロー情報を表示するコンポーネントを作成し、アプリケーション内に配置してみます。
Heroesコンポーネントを作成する
dotnet cliを使ってrazorコンポーネントを作成します。
dotnet new razorcomponent --output BlazorTourOfHeroes/Shared --name Heroes
既存の
BlazorTourOfHeroes/Shared
ディレクトリの中にHeroes.razor
ファイルが出来上がります。Shared/Heroes.razor<h3>Heroes</h3> @code { }( @Qiita さんrazorシンタックスハイライト待ってます)
hero
変数を追加するコンポーネントに
hero
変数を追加します。private string hero = "Windstorm";
hero
を表示する既存のHTMLタグを消去して、
hero
変数に置き換えます。@heroHeroesコンポーネントを表示する
Heroesコンポーネントをトップページに表示するため、
index.razor
を変更します。Pages/Index.razor<Heroes></Heroes>こんなんできました
Heroクラスを作成する
名前以外の属性を格納するためHeroクラスを作成します。
Model/Hero.csnamespace BlazorTourOfHeroes.Model { public class Hero { public int Id { get; set; } public string Name { get; set; } } }Heroesコンポーネントに戻り、作成したHeroクラスを使うようにリファクタリングします。
Shared/Heroes.razor@using BlazorTourOfHeroes.Model @hero @code { private Hero hero = new Hero { Id = 1, Name = "Windstorm", }; }heroオブジェクトを表示する
テンプレートを変更してheroオブジェクトを表示するようにします。
Shared/Heroes.razor<h2>@hero.Name</h2> <div><span>id: </span>@hero.Id</div> <div><span>name: </span>@hero.Name</div>hero名を大文字にする
大文字にするのは、
String.ToUpper()
を呼ぶだけです。Shared/Heroes.razor<h2>@hero.Name.ToUpper()</h2>こんなんできました。
heroを編集できるようにする
<input>
テキストボックスを用意してheroを編集できるようにします。バインディング
Heroesコンポーネントをリファクタリングして次のようにします。
Shared/Heroes.razor<div><label>name: <input @bind="hero.Name" placeholder="name" /></label></div>こんなんできました
- 投稿日:2020-06-28T15:05:51+09:00
[Microsoft] 新規プロジェクトの作成 - Angularチュートリアル Tour of Heroes を Blazor で再実装する
Angularのチュートリアル: Tour of Heroes をBlazorを使って再実装してみます。
新規プロジェクトを作成する
dotnet CLIを使用して初期アプリケーションを作成します。
- 開発環境をセットアップします。
- ソリューションとプロジェクトを作成します。
- サーバを起動してアプリケーションを動かしてみます。
開発環境をセットアップする
こちらの手順に従って開発環境をセットアップします。
Macの場合は、brewを使えば一発です。
brew cask install dotnet-sdkソリューションとプロジェクトを作成する
テスト等も含めたいので、プロジェクトを作成する前にソリューションを作成します。
dotnet new sln -o BlazorTourOfHeroesディレクトリとソリューションファイルが出来上がるので移動します。
cd BlazorTourOfHeroes続いてBlazorプロジェクトとテストプロジェクトを作成します。
dotnet new blazorwasm -o BlazorTourOfHeroes dotnet new xunit -o BlazorTourOfHeroes.Testsソリューションに作成したプロジェクトを追加します。
dotnet sln add BlazorTourOfHeroes dotnet sln add BlazorTourOfHeroes.Testsgitignoreファイルを作成します。
dotnet new gitignoreあとはいつものようにgitを初期化してコミットしておきます。
サーバを起動する
サーバを起動してみます。
dotnet run -p BlazorTourOfHeroesブラウザを起動して https://localhost:5001 か http://localhost:5000 へアクセスします。
こんなんできました
こんな感じの画面が見られます。
画面を構成するコンポーネント
以下のようなrazorファイルが生成されています。
razorファイルひとつがコンポーネントになります。
Angularのように、スタイルシートを個別に持つことはできません。├── App.razor ├── Pages │ ├── Counter.razor │ ├── FetchData.razor │ └── Index.razor └── Shared ├── Heroes.razor ├── MainLayout.razor ├── NavMenu.razor └── SurveyPrompt.razor
App.razor
がAngularでいうところのルーティングを担っています。変更してみる
トップページのタイトルを変更する
Index.razor
ファイルを編集します。Pages/Index.razor<h1>Tour of Heroes</h1>スタイルを変更する
スタイルシートはアプリケーション全体で一つです。
wwwroot/css/app.cssh1 { color: #369; font-family: Arial, Helvetica, sans-serif; font-size: 250%; } h2, h3 { color: #444; font-family: Arial, Helvetica, sans-serif; font-weight: lighter; }
- 投稿日:2020-06-28T03:45:46+09:00
WinFormsアプリを.NET Coreと.NET Frameworkで並行開発可能にする
日記に近いメモです。
前置き
メンテナンスしている、とあるWinFormsアプリは.NET Frameworkで動作しているのですが、開発者としては.NET 5に備え.NET Core 3.1に移行したいところです。
しかし、手元のWinFormsアプリはVisual Studio 2017 Expressで開発しているので、そう簡単に.NET Core(の新しいcsproj形式)に移行できません。
WinFormsデザイナーが使用できないと、UIに手を入れるのが非常に面倒になります。そこで「移行」するのではなく、Visual Studio 2017(.NET Framework)で開発可能なまま.NET Coreでもビルドできる状態を目指していきます。
(使用していないソースコードを削除する以外は、ソースコードは変えません。)
Visual Studio 2019が使えればこんな面倒なことはしません。対象のソースコード
下記のようなディレクトリ構成だとします。細かいファイルは省略してます。
なお、このアプリはx86でしか動作しませんが、開発環境はx64です。SolutionFolder/ ├───MyApps.sln ├───MyFormsApp/ │ ├───MyForms.csproj │ ├───Form1.cs │ ├───... │ └───Program.cs └───MyLib/ └───MyLib.csproj準備
基本的にはWindows フォーム デスクトップ アプリを .NET Core に移植する方法を参考にします。
.NET Portability Analyzer
.NET Portability Analyzer で事前に検証すべきですが、最近はWindows 互換機能パックにより互換性が高めなので、明らかに.NET Coreで使用できない機能(WCFとか)を使用していないのであれば、自己責任でやらなくてもいいかもしれません。
.NET Core SDK
アプリはx86でしか動作しませんが、開発環境はx64なので.NET Core 3.1 SDKをインストールします。
dotnet
コマンドが動作すること確認しておきます。PS > dotnet --list-sdks 3.1.100 [C:\Program Files\dotnet\sdk] PS > dotnet --list-runtimes Microsoft.AspNetCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]csprojに含まれないソースコードを削除する
.NET Coreではワイルドカードでソースコードを指定するため、使用していないソースコード(ゴミ)が残っている場合は削除しておきます。
新しいcsprojを作成する
docs.microsoft.comの例に倣い、
MyFormsAppCore
というディレクトリを作成して、その中に.NET Core用のプロジェクトを作ります。MyFormsAppの中に作ると、
dotnet
コマンドで毎回プロジェクトファイルを指定しないと行けないので面倒になります。SolutionFolder/ ├───MyApps.sln ├───MyFormsApp/ │ ├───MyForms.csproj │ ├───Form1.cs │ ├───... │ └───Program.cs ├───MyLib/ │ └───MyLib.csproj └───MyFormsAppCore/ <--- New folder for core projectPS > cd SolutionFolder/MyFormsAppCore PS SolutionFolder\MyFormsAppCore> New-Item -Type File MyFormsAppCore.csproj
dotnet new winforms
コマンドで作成してもいいんですが、
どうせ*.cs
ファイルを消して.csproj
ファイルを作成する必要があるので直接作ります。csprojの編集
下記を貼り付けます。
MyFormsAppCore.csproj<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>netcoreapp3.1</TargetFramework> <UseWindowsForms>true</UseWindowsForms> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <AssemblyName>MyCoreApp</AssemblyName> <RootNamespace>WindowsFormsApp1</RootNamespace> <Deterministic>false</Deterministic> <LangVersion>7.3</LangVersion> <PlatformTarget>x86</PlatformTarget> </PropertyGroup> <ItemGroup> <Compile Include="..\MyFormsApp\**\*.cs" Exclude="..\MyFormsApp\obj\**\*.cs" /> <EmbeddedResource Include="..\MyFormsApp\**\*.resx" /> </ItemGroup> </Project>
AssemblyName
/RootNamespace
は実際のプロジェクトに合わせて(古いcsprojファイルを参照して)変えます。LangVersion
は Visual Studio 2017で使用可能な7.3までにします。AssemblyVersion.cs
で、[assembly: AssemblyVersion("1.0.*")]
のようなワイルドカードを使用していたので、エラー CS8357が発生します。今回はソースコードは変えない前提なので、<Deterministic>false</Deterministic>
を追加します。- Visual Studio 2017でビルドしていると、
obj
ディレクトリ内に自動生成された*.cs
ファイルが作成されるので、Exclude
属性で除外しています。NuGet参照
使用するライブラリの精査が面倒なら、 Windows 互換機能パックへの参照を追加します。
PS SolutionFolder\MyFormsAppCore> dotnet add package Microsoft.Windows.Compatibility手元のプロジェクトだとMEFを使用していたので、
PS SolutionFolder\MyFormsAppCore> dotnet add package System.Compositionだけで済みました。
プロジェクト参照
使用するライブラリへの参照を追加します。
元のMyFormsApp.csproj
の中から、..\MyLib\MyLib.csproj
を指定しているItemGroup
タグをコピーして、MyFormsAppCore.csproj
に貼り付けます。... <ItemGroup> <ProjectReference Include="..\MyLib\MyLib.csproj"> <Project>{8E29561D-E424-4AAB-A4FA-E966EF653A0F}</Project> <Name>MyLib</Name> </ItemGroup> ...ライブラリ自体は.NET Frameworkのままですが、結果的に手元のプロジェクトは何もせずにビルドできました。
正しくは事前に.NET Coreプロジェクト化したcsprojを用意して、dotnet add reference
で追加する必要があるでしょう。ビルド
上手く行っていれば、問題なくビルドできるはずです。
# Debugビルド PS SolutionFolder\MyFormsAppCore> dotnet build -r win-x86 # Releaseビルド PS SolutionFolder\MyFormsAppCore> dotnet build -c Reelase -r win-x86実行
PS SolutionFolder\MyFormsAppCore> dotnet run -r win-x86発行
発行する方法はいくつかありますが、batファイルを用意することにします。
dotnet publish -c Release -r win-x86 --output publish実行後、
SolutionFolder\MyFormsAppCore\publish\MyCoreApp.exe
が作成されているので、それを実行します。
PublishTrimmed
やPublishSingleFile
を使用したければ、dotnet publishコマンドのリファレンスに従って、オプションは調整しましょう。
- 投稿日:2020-06-28T03:45:46+09:00
Visual Studio 2017でビルドしているWinFormsアプリを.NET Coreでも並行開発可能にする
日記に近いメモです。
前置き
メンテナンスしている、とあるWinFormsアプリは.NET Frameworkで動作しているのですが、開発者としては.NET 5に備え.NET Core 3.1に移行したいところです。
しかし、手元のWinFormsアプリはVisual Studio 2017 Expressで開発しているので、そう簡単に.NET Core(の新しいcsproj形式)に移行できません。
WinFormsデザイナーが使用できないと、UIに手を入れるのが非常に面倒になります。そこで「移行」するのではなく、Visual Studio 2017(.NET Framework)で開発可能なまま.NET Coreでもビルドできる状態を目指していきます。
(使用していないソースコードを削除する以外は、ソースコードは変えません。)
Visual Studio 2019が使えればこんな面倒なことはしません。対象のソースコード
下記のようなディレクトリ構成だとします。細かいファイルは省略してます。
なお、このアプリはx86でしか動作しませんが、開発環境はx64です。SolutionFolder/ ├───MyApps.sln ├───MyFormsApp/ │ ├───MyForms.csproj │ ├───Form1.cs │ ├───... │ └───Program.cs └───MyLib/ └───MyLib.csproj準備
基本的にはWindows フォーム デスクトップ アプリを .NET Core に移植する方法を参考にします。
.NET Portability Analyzer
.NET Portability Analyzer で事前に検証すべきですが、最近はWindows 互換機能パックにより互換性が高めなので、明らかに.NET Coreで使用できない機能(WCFとか)を使用していないのであれば、自己責任でやらなくてもいいかもしれません。
.NET Core SDK
アプリはx86でしか動作しませんが、開発環境はx64なので.NET Core 3.1 SDKをインストールします。
dotnet
コマンドが動作すること確認しておきます。PS > dotnet --list-sdks 3.1.100 [C:\Program Files\dotnet\sdk] PS > dotnet --list-runtimes Microsoft.AspNetCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]csprojに含まれないソースコードを削除する
.NET Coreではワイルドカードでソースコードを指定するため、使用していないソースコード(ゴミ)が残っている場合は削除しておきます。
新しいcsprojを作成する
docs.microsoft.comの例に倣い、
MyFormsAppCore
というディレクトリを作成して、その中に.NET Core用のプロジェクトを作ります。MyFormsAppの中に作ると、
dotnet
コマンドで毎回プロジェクトファイルを指定しないと行けないので面倒になります。SolutionFolder/ ├───MyApps.sln ├───MyFormsApp/ │ ├───MyForms.csproj │ ├───Form1.cs │ ├───... │ └───Program.cs ├───MyLib/ │ └───MyLib.csproj └───MyFormsAppCore/ <--- New folder for core projectPS > cd SolutionFolder/MyFormsAppCore PS SolutionFolder\MyFormsAppCore> New-Item -Type File MyFormsAppCore.csproj
dotnet new winforms
コマンドで作成してもいいんですが、
どうせ*.cs
ファイルを消して.csproj
ファイルを作成する必要があるので直接作ります。csprojの編集
下記を貼り付けます。
MyFormsAppCore.csproj<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>netcoreapp3.1</TargetFramework> <UseWindowsForms>true</UseWindowsForms> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <AssemblyName>MyCoreApp</AssemblyName> <RootNamespace>WindowsFormsApp1</RootNamespace> <Deterministic>false</Deterministic> <LangVersion>7.3</LangVersion> <PlatformTarget>x86</PlatformTarget> <RuntimeIdentifier>win-x86</RuntimeIdentifier> </PropertyGroup> <ItemGroup> <Compile Include="..\MyFormsApp\**\*.cs" Exclude="..\MyFormsApp\obj\**\*.cs" /> <EmbeddedResource Include="..\MyFormsApp\**\*.resx" /> </ItemGroup> </Project>
AssemblyName
/RootNamespace
は実際のプロジェクトに合わせて(古いcsprojファイルを参照して)変えます。LangVersion
は Visual Studio 2017で使用可能な7.3までにします。AssemblyVersion.cs
で、[assembly: AssemblyVersion("1.0.*")]
のようなワイルドカードを使用していたので、エラー CS8357が発生します。今回はソースコードは変えない前提なので、<Deterministic>false</Deterministic>
を追加します。- Visual Studio 2017でビルドしていると、
obj
ディレクトリ内に自動生成された*.cs
ファイルが作成されるので、Exclude
属性で除外しています。- 前述の通り、x86でしか動作しないWinFormsアプリなので
RuntimeIdentifier
とPlatformTarget
を指定しています。x64でも動作させるなら、指定は外しておくべきでしょう。NuGet参照
使用するライブラリの精査が面倒なら、 Windows 互換機能パックへの参照を追加します。
PS SolutionFolder\MyFormsAppCore> dotnet add package Microsoft.Windows.Compatibility手元のプロジェクトだとMEFを使用していたので、
PS SolutionFolder\MyFormsAppCore> dotnet add package System.Compositionだけで済みました。
プロジェクト参照
使用するライブラリへの参照を追加します。
元のMyFormsApp.csproj
の中から、..\MyLib\MyLib.csproj
を指定しているItemGroup
タグをコピーして、MyFormsAppCore.csproj
に貼り付けます。... <ItemGroup> <ProjectReference Include="..\MyLib\MyLib.csproj"> <Project>{8E29561D-E424-4AAB-A4FA-E966EF653A0F}</Project> <Name>MyLib</Name> </ItemGroup> ...ライブラリ自体は.NET Frameworkのままですが、結果的に手元のプロジェクトは何もせずにビルドできました。
正しくは事前に.NET Coreプロジェクト化したcsprojを用意して、dotnet add reference
で追加する必要があるでしょう。ビルド
上手く行っていれば、問題なくビルドできるはずです。
# Debugビルド PS SolutionFolder\MyFormsAppCore> dotnet build # Releaseビルド PS SolutionFolder\MyFormsAppCore> dotnet build -c Reelase実行
PS SolutionFolder\MyFormsAppCore> dotnet run発行
発行する方法はいくつかありますが、batファイルを用意することにします。
dotnet publish -c Release --output publish実行後、
SolutionFolder\MyFormsAppCore\publish\MyCoreApp.exe
が作成されているので、それを実行します。
PublishTrimmed
やPublishSingleFile
を使用したければ、dotnet publishコマンドのリファレンスに従って、オプションは調整しましょう。
- 投稿日:2020-06-28T03:45:46+09:00
Visual Studio 2017でビルドしているWinFormsアプリを.NET Coreでも並行開発可能にする方法
日記に近いメモです。
前置き
メンテナンスしている、とあるWinFormsアプリは.NET Frameworkで動作しているのですが、開発者としては.NET 5に備え.NET Core 3.1に移行したいところです。
しかし、手元のWinFormsアプリはVisual Studio 2017 Expressで開発しているので、そう簡単に.NET Core(の新しいcsproj形式)に移行できません。
WinFormsデザイナーが使用できないと、UIに手を入れるのが非常に面倒になります。そこで「移行」するのではなく、Visual Studio 2017(.NET Framework)で開発可能なまま.NET Coreでもビルドできる状態を目指していきます。
(使用していないソースコードを削除する以外は、ソースコードは変えません。)
Visual Studio 2019が使えればこんな面倒なことはしません。対象のソースコード
下記のようなディレクトリ構成だとします。細かいファイルは省略してます。
なお、このアプリはx86でしか動作しませんが、開発環境はx64です。SolutionFolder/ ├───MyApps.sln ├───MyFormsApp/ │ ├───MyForms.csproj │ ├───Form1.cs │ ├───... │ └───Program.cs └───MyLib/ └───MyLib.csproj準備
基本的にはWindows フォーム デスクトップ アプリを .NET Core に移植する方法を参考にします。
.NET Portability Analyzer
.NET Portability Analyzer で事前に検証すべきですが、最近はWindows 互換機能パックにより互換性が高めなので、明らかに.NET Coreで使用できない機能(WCFとか)を使用していないのであれば、自己責任でやらなくてもいいかもしれません。
.NET Core SDK
アプリはx86でしか動作しませんが、開発環境に合わせて.NET Core 3.1 SDK x64をインストールします。
dotnet
コマンドが動作すること確認しておきます。PS > dotnet --list-sdks 3.1.100 [C:\Program Files\dotnet\sdk] PS > dotnet --list-runtimes Microsoft.AspNetCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]csprojに含まれないソースコードを削除する
.NET Coreではワイルドカードでソースコードを指定するため、使用していないソースコード(ゴミ)が残っている場合は削除しておきます。
新しいcsprojを作成する
docs.microsoft.comの例に倣い、
MyFormsAppCore
というディレクトリを作成して、その中に.NET Core用のプロジェクトを作ります。MyFormsAppの中に作ると、
dotnet
コマンドで毎回プロジェクトファイルを指定しないと行けないので面倒になります。SolutionFolder/ ├───MyApps.sln ├───MyFormsApp/ │ ├───MyForms.csproj │ ├───Form1.cs │ ├───... │ └───Program.cs ├───MyLib/ │ └───MyLib.csproj └───MyFormsAppCore/ <--- New folder for core projectPS > cd SolutionFolder/MyFormsAppCore PS SolutionFolder\MyFormsAppCore> New-Item -Type File MyFormsAppCore.csproj
dotnet new winforms
コマンドで作成してもいいんですが、
どうせ*.cs
ファイルを消して.csproj
ファイルを作成する必要があるので直接作ります。csprojの編集
下記を貼り付けます。
MyFormsAppCore.csproj<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>netcoreapp3.1</TargetFramework> <UseWindowsForms>true</UseWindowsForms> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <AssemblyName>MyCoreApp</AssemblyName> <RootNamespace>WindowsFormsApp1</RootNamespace> <Deterministic>false</Deterministic> <LangVersion>7.3</LangVersion> <PlatformTarget>x86</PlatformTarget> <RuntimeIdentifier>win-x86</RuntimeIdentifier> </PropertyGroup> <ItemGroup> <Compile Include="..\MyFormsApp\**\*.cs" Exclude="..\MyFormsApp\obj\**\*.cs" /> <EmbeddedResource Include="..\MyFormsApp\**\*.resx" /> </ItemGroup> </Project>
AssemblyName
/RootNamespace
は実際のプロジェクトに合わせて(古いcsprojファイルを参照して)変えます。LangVersion
は Visual Studio 2017で使用可能な7.3までにします。AssemblyVersion.cs
で、[assembly: AssemblyVersion("1.0.*")]
のようなワイルドカードを使用していたので、エラー CS8357が発生します。今回はソースコードは変えない前提なので、<Deterministic>false</Deterministic>
を追加します。- Visual Studio 2017でビルドしていると、
obj
ディレクトリ内に自動生成された*.cs
ファイルが作成されるので、Exclude
属性で除外しています。- 前述の通り、x86でしか動作しないので
PlatformTarget
にx86
を指定しています。- x86でしか動作しないWinFormsアプリなので、コマンドラインでの指定を省略できるように
RuntimeIdentifier
にwin-x86
を指定しています。x64でも動作させるなら、指定は外しておくべきでしょう。追加の編集
デフォルトで、ある程度使える設定はされていますが、何も設定をしなくていいわけではありません。
下記の設定(タグ)を、旧csprojからコピペが必要でした。
パスも調整する必要があります。
ApplicationIcon
ApplicationManifest
NuGet参照
使用するライブラリの精査が面倒なら、 Windows 互換機能パックへの参照を追加します。
PS SolutionFolder\MyFormsAppCore> dotnet add package Microsoft.Windows.Compatibility手元のプロジェクトだとMEFを使用していたので、
PS SolutionFolder\MyFormsAppCore> dotnet add package System.Compositionだけで済みました。
プロジェクト参照
使用するライブラリへの参照を追加します。
元のMyFormsApp.csproj
の中から、..\MyLib\MyLib.csproj
を指定しているItemGroup
タグをコピーして、MyFormsAppCore.csproj
に貼り付けます。... <ItemGroup> <ProjectReference Include="..\MyLib\MyLib.csproj"> <Project>{8E29561D-E424-4AAB-A4FA-E966EF653A0F}</Project> <Name>MyLib</Name> </ItemGroup> ...ライブラリ自体は.NET Frameworkのままですが、結果的に手元のプロジェクトは何もせずにビルドできました。
正しくは事前に.NET Coreプロジェクト化したcsprojを用意して、dotnet add reference
で追加する必要があるでしょう。ビルド
上手く行っていれば、問題なくビルドできるはずです。
# Debugビルド PS SolutionFolder\MyFormsAppCore> dotnet build # Releaseビルド PS SolutionFolder\MyFormsAppCore> dotnet build -c Reelase実行
PS SolutionFolder\MyFormsAppCore> dotnet run発行
発行する方法はいくつかありますが、batファイルを用意することにします。
dotnet publish -c Release --output publish実行後、
SolutionFolder\MyFormsAppCore\publish\MyCoreApp.exe
が作成されているので、それを実行します。
PublishTrimmed
やPublishSingleFile
を使用したければ、dotnet publishコマンドのリファレンスに従って、オプションは調整しましょう。