タグ別アーカイブ: C#

C#でWindowsサービスのexeにインストーラと自動起動を組み込む

はじめに

C#でのWindowsサービス開発のはじめ方でとりあえずサービスの開発はできるのだが、インストールがめんどくさいのとインストールしたあとに自動で起動してくれないので、これをexe自身でできるように機能を組み込んでみる。

細かいことを考えるよりテンプレートとして使うほうがラクなので、一番下にProgram.csの内容をすべて載せておく。簡単な説明を見たあとでもいいし、面倒なら丸ごとコピペすればそれなりに動くと思う。

主要クラスの説明

インストール・アンインストールはManagedInstallerClassクラス

サービスをインストール・アンインストールにはManagedInstallerClassクラスのInstallHelper(string[])メソッドを使う。このメソッドは内部的にInstallUtil.exeを呼び出しているらしい。

インストール時は引数にexeのパスだけ、アンインストール時は”/u”とexeのパスを渡すだけだ。

var myAssembly = System.Reflection.Assembly.GetEntryAssembly();
string path = myAssembly.Location;
var param = (isInstallMode) ? new[] { path } : new[] { "/u", path };
ManagedInstallerClass.InstallHelper(param);

起動(開始)・停止はServiceControllerクラス

サービスの開始や停止にはServiceControllerクラスのStart()メソッド、Stop()メソッドを使う。ServiceControllerのコンストラクタにサービス名を渡すだけでよい。

起動(開始)

ServiceController sc = new ServiceController(SERVICE_NAME);
if (sc.Status != ServiceControllerStatus.Running)
{
    sc.Start();
}

停止

ServiceController sc = new ServiceController(SERVICE_NAME);
if (sc.Status != ServiceControllerStatus.Stopped)
{
    sc.Stop();
}

全体的なロジック

Mainメソッドではコマンドライン引数が渡されていれば、コンソールモードとして起動する。

コンソールモード(RunAsConsoleModeメソッド内)では引数に応じて、インストール(/i)・アンインストール(/u)・開始(/start)・停止(/stop)を行う。

インストールでは終了後に自動的に開始し、アンインストールでは停止してからアンインストールを行うようにしてある。

ワンポイント

呼び出し方

exeファイル名をAwesomeService.exeだとすると

AwesomeService.exe /i

などとなる。

ソースコード (Program.cs)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration.Install;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;

namespace WindowsService1
{
    static class Program
    {
        public const string SERVICE_NAME = "AwesomeService";
        public const string DISPLAY_NAME = "Awesome Service";

        static void Main(string[] args)
        {
            // Run as console mode when an argument is provided.
            if (1 <= args.Length)
            {
                RunAsConsoleMode(args[0]);
                return;
            }

            // Run as Windows Service
            ServiceBase.Run(new Service1()
            {
                CanShutdown = true,
                CanPauseAndContinue = false,
            });
        }

        /// <summary>
        /// Run as console mode when an argument is provided.
        /// </summary>
        /// <param name="arg"></param>
        /// <returns></returns>
        static void RunAsConsoleMode(string arg)
        {
            string mode = arg.ToLower();
            var myAssembly = System.Reflection.Assembly.GetEntryAssembly();
            string path = myAssembly.Location;

            if (mode == "/i" || mode == "/u")
            {
                bool isInstallMode = (mode == "/i");
                var mes = (isInstallMode) ? "installed" : "uninstalled";
                if (IsServiceExists())
                {
                    Console.WriteLine("{0} has been already {1}.", DISPLAY_NAME, mes);
                }
                else
                {
                    if (!isInstallMode) { StopService(); }
                    var param = (mode == "/i") ? new[] { path } : new[] { "/u", path };
                    ManagedInstallerClass.InstallHelper(param);
                    Console.WriteLine("{0} has been successfully {1}.", DISPLAY_NAME, mes);
                    if (isInstallMode) { StartService(); }
                }
            }
            else if (mode == "/start")
            {
                StartService();
            }
            else if (mode == "/stop")
            {
                StopService();
            }
            else
            {
                Console.WriteLine("Provided arguments are unrecognized.");
            }
        }

        /// <summary>
        /// Whether the service already exists in the computer or not.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        static bool IsServiceExists(string name = SERVICE_NAME)
        {
            ServiceController[] services = ServiceController.GetServices();
            return services.Any(s => s.ServiceName == name);
        }

        /// <summary>
        /// Start the service.
        /// </summary>
        /// <returns></returns>
        static void StartService()
        {
            if (IsServiceExists())
            {
                Console.WriteLine("Starting {0}...", SERVICE_NAME);
                ServiceController sc = new ServiceController(SERVICE_NAME);
                if (sc.Status == ServiceControllerStatus.Running)
                {
                    Console.WriteLine("{0} has been already started.", DISPLAY_NAME);
                }
                else
                {
                    try
                    {
                        sc.Start();
                        Console.WriteLine("{0} has been started.", DISPLAY_NAME);
                    }
                    catch (Exception)
                    {
                        Console.WriteLine("{0} could not be started.", DISPLAY_NAME);
                    }
                }
            }
        }

        /// <summary>
        /// Stop the service.
        /// </summary>
        /// <returns></returns>
        static void StopService()
        {
            if (IsServiceExists())
            {
                Console.WriteLine("Stopping {0}...", SERVICE_NAME);
                ServiceController sc = new ServiceController(SERVICE_NAME);
                if (sc.Status == ServiceControllerStatus.Stopped)
                {
                    Console.WriteLine("{0} has been already stopped.", DISPLAY_NAME);
                }
                else
                {
                    try
                    {
                        sc.Stop();
                        Console.WriteLine("{0} has been stopped.", DISPLAY_NAME);
                    }
                    catch (Exception)
                    {
                        Console.WriteLine("{0} could not be stopped.", DISPLAY_NAME);
                    }
                }
            }
        }

    }

    [RunInstaller(true)]
    public class ProjectInstaller : Installer
    {
        public ProjectInstaller()
        {
            var spi = new ServiceProcessInstaller
            {
                Account = ServiceAccount.LocalSystem
            };
            var si = new ServiceInstaller
            {
                ServiceName = Program.SERVICE_NAME,
                DisplayName = Program.DISPLAY_NAME,
                Description = "Awesome Service.",
                StartType = ServiceStartMode.Automatic,
            };
            this.Installers.Add(spi);
            this.Installers.Add(si);
        }
    }
}

参考

C#でWindowsサービス開発のはじめ方

準備

  • 表示名 (DisplayName):サービス一覧などで「名前」に表示される名前(スペースを含んでもよいし、日本語も可)
  • サービス名 (ServiceName):サービスの識別用の名前(英数字のみ。スペースなども含まない)

プロジェクトの作成

プロジェクトの新規作成で右上の検索ボックスに “サービス” と入力するのが一番手っ取り早い。

通常通り、デフォルトではプロジェクト名がサービスのexe名になるので、先に考えたサービス名を入力しておく。

ここでデザイナからインストーラクラスを追加することもできるが、コードで書いたほうが短くて使いやすいので、コード側で用意することにする。

Program.csの編集

Program.csを開く。

Mainメソッドはそのままでもかまわないが、なぜか冗長な書き方なので、下記のように変更する。

CanShutdowntrueにしておくとPCがシャットダウンするときのイベントを拾うことができる。凝った作りにしない限り一時停止と再開はできなくてもいいと思うので、適宜CanPauseAndContinuefalseにしておく。

static void Main(string[] args)
{
    ServiceBase.Run(new SKYMAgentService()
    {
        CanShutdown = true,
        CanPauseAndContinue = false,
    });
}

Programクラスの下に下記のProjectInstallerクラスを追加する(誤ってProgramクラスの中に書かないこと)。当然別ファイルに分けてもよい。

[RunInstaller(true)]
public class ProjectInstaller : Installer
{
    public ProjectInstaller()
    {
        var spi = new ServiceProcessInstaller
        {
            Account = ServiceAccount.LocalSystem
        };
        var si = new ServiceInstaller
        {
            ServiceName = "サービス名",
            DisplayName = "表示名",
            Description = "サービスの説明",
            StartType = ServiceStartMode.Automatic,
        };
        this.Installers.Add(spi);
        this.Installers.Add(si);
    }
}

Serviceクラスの実装

Service1.csのデザイナのプロパティにServiceNameがあるのでこれもサービス名に統一しておく。

デザイナからコードビューに切り替えるとOnStartOnStopが存在しているので、この中に開始時の処理と停止時の処理を書く。OnShutdownが必要な場合は下記のように追加する。

protected override void OnShutdown()
{
    server.Stop();
}

あとはビルドすればbin\Debugかbin\Releaseにexeが生成される。

インストールとアンインストール

インストールとアンインストールにはInstalUtil.exeを使う。このコマンドはVSにパスの通ったコマンドプロンプトでないといけないので、スタート→プログラム→VS→Visual Studio コマンドプロンプトを管理者権限で起動する。

cd "exeのあるフォルダ"
installutil "exeパス"

でインストールできる。ちなみにアンインストールはinstallutil /u "exeパス"でできる。

参考

バインディングオブジェクト内の変更をコントロールに反映するには

概要

WPFでコントロールにオブジェクトをBindingしている場合、オブジェクトのプロパティが変更されたときにコントロールの表示も伴って更新されてほしいわけだが、残念ながら更新されない。

これは、更新されたことを通知する機構がオブジェクトに備わっていないからである。 このページではこれの対応策を示す。

対策

まず INotifyPropertyChanged インターフェースを実装

コントロールにプロパティが変更されたことを通知するには、まずバインドするクラスにINotifyPropertyChangedインターフェースを実装しておく。

INotifyPropertyChangedを実装するとPropertyChangedイベントが追加されるので、これをコールするための下記のようなメソッドを作っておく。

public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(string name)
{
    if (PropertyChanged != null)
    {
        PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(name));
    }
}

この引数nameには変更されたプロパティの名前を渡す。

そして変更を通知すべきプロパティ(表示を更新する必要のあるプロパティ)のsetアクセサで次のようにコールする。

private string _userId;
public string UserId
{
    get
    {
        return _userId;
    }
    set
    {
        _userId = value;
        OnPropertyChanged("UserId");
    }
}

これでUserIdに値がセットされると、変更が通知されるようになる。

コレクションには ObservableCollection<T> クラス

たとえばTreeViewに階層構造のオブジェクトをバインドしているとする。

オブジェクトの中のコレクションをList<T>などで実装しているとこれらのコレクションに要素が追加もしくは削除されたとき、TreeViewの表示が更新されない。 また、これはコレクションのプロパティからPropertyChangedを呼んでも同じである。

これを自動的に反映されるようにするにはコレクションにObservableCollection<T>クラスを利用すればいい。

ソースコード

以上の実装例を示す。

public class TopLevel : System.ComponentModel.INotifyPropertyChanged
{
    private string _userId;
    public string UserId
    {
        get
        {
            return _userId;
        }
        set
        {
            _userId = value;
            OnPropertyChanged("UserId");
        }
    }
    public ObservableCollection<SecondLevel> SecondLevels { get; set; }

    #region INotifyPropertyChanged メンバー
    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(name));
        }
    }
    #endregion
}

参考