C#에서 만든 .DLL 라이브러리를 MFC(C++)에서 사용할려면 .tlb 파일을 생성 및 윈도우즈 레지스트리에 등록하는 과정이 필요하다.

이를 위해서는 MS 제공 RegAsm.exe라는 프로그램을 이용해서 처리를 하게 될텐데(자세한 정보는 여기를 클릭) 이를 위해서 Developer Command Prompt for VS 2017라는 일종의 커맨더 창에서 작업을 하게된다.

이는 개발자가 개발 단계에서는 문제가 되지 않으나 개발된 프로그램을 일반 사용자에게 배포하게 될때는 일반 사용자가 이 작업을 할수는 없는 것이다.

따라서 프로그램상에서 .DLL로부터 .tlb 생성 및 레지스트리 등록을 처리해야 할 것이다.


본 포스트는 C# 코드상에서 이 두 가지 작업을 수행하는 방법에 대해 정리하고자 한다.

C#에서 외부 실행 파일을 실행하는 것 자체는 그렇게 어려운 작업은 아니나 문제는 레지스트리에 뭔가를 쓰게 되는 일은 시스템의 보안상 간단하게 처리되어질수는 없는 일이기에 코드상에서 처리하는 일에 복잡성이 발생하게 된다.

이러한 상황 가운데서 위 2가지 작업을 프로그램적으로 어떻게 처리할수 있는지를 살피고자 한다.


핵심은 프로그램 자체가 '관리자 권한'으로 실행되는 프로그램이어야 한다는 것이다. 이렇게 되어야 코드를 통해서 레지스트리 등록이 가능해 진다.


(1) 관리자 권한으로 실행되는 프로그램 만들기

Visual Studio 2017을 기준으로 설명하면 '솔루션 탐색기' 탭에서 해당 프로젝트 이름위에 마우스 우 클릭 ⇒ 속성 ⇒ 보안 ⇒ "ClickOnce 보안 설정 사용"을 체크 ⇒ 이 상태를 저장(Ctrl-S)



이상의 작업을 거치면 '솔루션 탐색기' 탭의 프로젝트 명 아래의 Properties 항목 아랫쪽에 app.manifest라는 파일이 생성이 된다.

이 파일의 설정 값을 조정함을 통해서 현재의 프로그램이 관리자 권한으로 실행되는 프로그램으로 설정된다.



아래 그림의 노랑색 밑줄친 부분의 level의 값 asInvoker를 requireAdministrator로 변경을 해 준 후 저장을 한다.



Visual Studio 2017을 종료 후 '관리자 권한'으로 새로 시작한다.

(그렇지 않고 현재 상태에서 프로그램을 디버깅(실행)하면 "다른 자격 증명을 사용하여 다시 시작"할 것인지 묻는 창이 뜬다. 이 창이 의미하는 것은 Visual Studio 2017을 관리자 권한으로 다시 실행하겠느냐는 뜻이다.)


프로그램을 디버깅(실행)하면 현재 프로그램의 .exe 실행 파일이 생성이 되고 이 실행 파일은 매 실행시마다 관리자 권한으로 실행 여부를 사용자에게 묻게 된다.


(2) .tlb 파일 생성 및 윈도우즈 레지스트리 등록하는 소스 코드


using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

using System.Text;

using System.Threading.Tasks;


namespace RegisterTLB

{

    class Program

    {

        static void Main(string[] args)

        {

            Console.WriteLine("This program is registering .TLB to Windows registry.");


            var proc = new ProcessStartInfo();


            // .dll을 이용해서 .tlb생성 및 레지스트리 등록 명령어

            // regasm의 사용법은 DOS창(커맨더 창)에서 regasm /?을 하면 

            // regasm을 아래와같이 이 파일의 경로를 표시하지 않으면 현재의 프로그램의 실행 파일과 같은 위치에

            // regasm.exe가 있어야 된다.

            string mCmd = "regasm EthernetClientLib.dll /tlb:EthernetClientLib.tlb";

            proc.UseShellExecute = true;


            //아래 경로에 .DLL가 있어야하고 .tlb가 이 위치에 생성이 되고 regasm.exe도 이 위치에서

            //작업을 하게된다.

            proc.WorkingDirectory = @"D:\Joe\CSharp\EthernetClientLib\EthernetClientLib\bin\Debug";


            //위의 regasm EthernetClientLib.dll /tlb:EthernetClientLib.tlb를 실행시킬 명령어

            proc.FileName = "cmd.exe";


            //아래 속성의 runas 자체가 관리자 권한으로 실행시키겠다는 설정값이다.

            proc.Verb = "runas";

            proc.Arguments = "/C " + mCmd;

            proc.WindowStyle = ProcessWindowStyle.Hidden;

            Process rt = Process.Start(proc);


            Debug.WriteLine("##### .tlb 생성 및 레지스트리 등록을 마쳤습니다.");

        }

    }

}



C# .dll library에서 MessageBox 사용하기(메시지 창 띄우기)


C#의 프로젝트를 클래스 라이브러리(.dll 라이브러리)로 생성하면 기본적으로 MessageBox를 띄울수 없다.

그런데 C# .dll 라이브러리 단에서 굳이 메시지 창을 띄워야 할 경우가 있을 것이다. 그게 여러모로 편리할 것이다.

이런 경우에 대한 해법이다.


프로젝트 명에 우측 마우스 - 추가 - 참조 - 어셈블리 - 프레임워크 - System.Windows.Forms를 체크해 주면

아래 클래스가 자동으로 using에 포함된다.

만일 자동으로 추가되지 않으면 아래 내용을 직접 타이핑해서 추가해 주면 된다.


using System.Windows.Forms;





이후에 필요한 곳에서 아래와 같이 사용하면 되겠다.


MessageBox.Show("해당하는 이미지가 없습니다.", "적당한 제목");


(C# 프로그램을 .dll Library로 만드는 방법은 여기를 클릭)


delegate는 C#이 가진 독특한 개념인데 따라서 기본적으로 생소한 개념으로 다가온다.

아래는 delegate에 대한 기본적인 개념 소개와 간단한 예제를 통해 개념을 이해해 보고자 한다.


using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

using System.Text;

using System.Threading.Tasks;


namespace EXdelegate1

{

    class Program

    {

        //delegate란 

        // (1) 일종의 데이터 type과 같다. 

        // (2) 어떤 메소드를 encapsulate할수 있다. 즉 어떤 메소드를 가리킬수 있고 이를 통해 그 메소드를 실행할수 있다.

        // (3) similar to function pointer in C, C++ 

        // (C, C++에서 어떤 함수를 포인터로 가리키고 이 포인터를 이용해서 해당 함수를 실행하는 방식)

        //아래는 새로운 데이터 타입과 같은 delegate를 선언하는데 이 새로운 데이터 타입의 이름은 Del이고

        //이 데이터 타입이 가리킬수 있는 메소드는 argument로 string을 하나 받고 반환 값은 void인

        //메소드가 있다면 그들 메소드들은 모두 delegate 이름이 Del인 새로운 데이터 타입으로 객체를 선언하고

        //이 선언된 delegate 타입의 객체로 그들 메소드들을 실행할수 있다.

        private delegate void Del(string message);


        //아래는 새로운 데이터 타입인 MyDel이라는 이름의 데이터 타입을 선언하는데 이 데이터 타입은 

        //특별히 delegate형태의 데이터 타입이다.

        //MyDel라는 데이터 타입으로 선언된 어떤 객체(참조변수)는 argument로 string 타입 하나, int 타입 하나를

        //받고 반환 데이터는 string 타입을 반환하는 모든 메소드를 MyDel이라는 데이터 타입으로 모두 

        //가리키고 또 실행할수 있다는 개념이다.

        private delegate string MyDel(string info, int some);


        static void Main(string[] args)

        {

            //delegate 객체를 생성. 이 delegate 객체가 실행할 메소드는 DelegateMethod()이다.

            //handler는 delegate Del 타입의 object이다.

            //delegate 타입의 객체 생성은 Java나 기존의 객체 생성 방식과 약간 다르다.

            //Del이라는 delegate 타입의 객체 handler가 실행할 메소드 이름을 대입해 주면 

            //delegate Del 타입의 새로운 객체가 하나 생성된다.

            Del handler = DelegateMethod;

            handler("delegate object가 보낸 메시지");


            MyDel another = AnotherDelegateMethod;


            Console.WriteLine("\n\n 이름 : " + another("홍길동", 25));


            Del myDel = mDel;

            myDel("Del이라는 이름의 delegate가 실행하는 두~~ 번째 메소드임");



            //익명함수 방식으로 delegate 객체 생성

            //익명함수란 원래 객체 생성 시점에 함수(메소드)의 이름은 없이 막바로 함수 본체가

            //주어지면서 객체 생성되는 형태이다.

            //따라서 아래에서 MyDel의 조건인 매개인자로 string 하나, int형 하나를 받고

            //반환 데이터 타입이 string이라는 조건만 만족시켜주면 함수 본체는 

            //내용이 어떠하든지 상관이 없다.

            MyDel anonymMyDel = delegate (string addr, int sex)

            {

                string gender = "";


                if (sex == 1)

                {

                    gender = "남성";

                } else if (sex == 0)

                {

                    gender = "여성";

                }

                return "주소 : " + addr + ", 성별 : " + gender;

            };


            Console.WriteLine(anonymMyDel("서울특별시 강남구 아무개길 77", 1));



            //익명함수 방식의 delegate 객체 생성2

            Del anonymDel = delegate (string msg)

            {

                Console.WriteLine("\n\nDel의 익명함수 방식 객체 : "+msg);

            };


            anonymDel("오 이런 식으로 되는구나\n\n\n");

        }



        private static void DelegateMethod(string msg)

        {

            Console.WriteLine("\n여기는 delegate method\nmessage sent from delegate object : \""+msg+"\"");

        }


        private static string AnotherDelegateMethod(string name, int age)

        {

            return name + ", age : " + age;

        }


        private static void mDel(string info)

        {

            Console.WriteLine("\n\n여기는 " + info + "~~~\n\n");

        }


    }

}




C#에서 .dll로 library를 만들고 이것을 C++에서 사용하기


(C++에서 C# .dll 라이브러리 사용하는 법)


C#은 MFC에 비해 여러면에서 개발자 편의적이다. 따라서 C#으로 만든 프로그램을 .dll 라이브러리로 만들고 이것을 C++ 혹은 MFC에서 사용하는 방법에 대해서 다루고자 한다.


전개순서는


-. C#을 클래스 라이브러리로 개발하기

-. C#을 .dll library로 만들기 위한 Visual Studio 2017에서 속성 설정하기

-. C# 프로그램이 .dll library로 MFC(혹은 C++)와 동작하기 위한 소스 코드 작성법

-. C# 소스코드 빌드하기

-. 빌드된 C#의 .dll을 레지스트리 등록 및 .tlb 파일 만들기

-. C++ 쪽에서 C#용 .dll 파일 사용하기 위한 소스 코드 작성법


의 순서로 진행이 된다.


1) C#을 클래스 라이브러리로 개발하기


Visual Studio 2017 파일 - 새로 만들기 - 프로젝트 - 클래스 라이브러리(.NET Framework)



이렇게해서 소스코드 창이 열리면 


2) C#을 .dll library로 만들기 위한 Visual Studio 2017에서 속성 설정하기
다음과 같이 해당 프로젝트의 속성을 C# 클래스 라이브러리(.dll) 용으로 설정해 준다.





3) C# 프로그램이 .dll library로 MFC(혹은 C++)와 동작하기 위한 소스 코드 작성법

소스 코드는 크게 두 부분으로 구성이 된다. C++과 통신하기 위한 interface 하나와 필요한 사용자 기능을 위한 class 하나이다. 이 둘에 대한 Guid 값을 지정해 주어야 하는데 Guid 값을 얻기 위해서는 아래 그림과 같이 하면된다.




'복사' 버튼을 클릭하여 Guid 값을 소스코드에(interface와 사용자 클래스에) 붙여넣으면 된다.

소스 코드는 아래와 같다. Guid가 붙여진 곳을 확인해보자.

여기서 또 기억할 사항은 Guid값을 소스코드에서 사용할수 있기 위해서는 using System.Runtime.InteropServices를 추가해 주어야 한다.


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Runtime.InteropServices;


namespace ExMakeClassLib

{

    //여기가 C++과 연동하기 위한 interface부분이다.

    [Guid("357CCEDB-44A1-481E-A42A-0D4DCA8C5EEA")]

    public interface ICallClass

    {

        //여기에 C++에서 사용할 메소드들의 프로토타입을 지정해 주면 된다.

        //C++에서 사용하게 될 public 형태의 메소드들이 여기에 해당된다.

        //private 모드의 메소드는 여기에 지정이 불가능하다. 

        //생성자는 여기서 지정해 줄 필요가 없다.

        //메소드들만 지정해 주면 된다.

        void setInfo(String _name, int age, String _phoneNum);

        void showInfo();

    }



    //여기가 사용자가 원하는 기능을 구현한 클래스이다.

    [Guid("D00C0769-26E7-4B4E-A7D0-8CAAE2AB3702")]

    public class Class2 : ICallClass

    {

        private String name;

        private int age;

        private String phoneNum;

        //여기서 주의 해야할 사항은 SerialPort나 Thread 같은경우 처음 사용 후에 

        //포트를 닫는 동작이 필요하거나 Thread를 종료시키는 동작이 필요할 경우 등

        //여러 메소드들에서 사용을 해야하는 멤버 변수들은 반드시 static으로

        //선언해 주어야 한다. 왜냐하면 비록 전역 변수 형태로 선언되었다 할지라도

        //처음 사용했던 메소드가 아닌 다른 메소드에서 또 사용되어질 경우는

        //해당 멤버 변수(Thread, SerialPort...)가 null 상태가 되어 버린다.

        //따라서 C++에서 계속해서 해당 멤버 변수를사용할수 있도록 하기 위해서는

        //반드시 static으로 선언해 주어야 한다.


        public Class2() { }


        public Class2(String _name, int _age, String _phoneNum)

        {

            name = _name;

            age = _age;

            phoneNum = _phoneNum;

        }


        public void showInfo()

        {

            Console.WriteLine("▶ name : " + name + "\n▶ age : " + age + "\n▶ 폰번호 : " + phoneNum);

            Console.WriteLine("---- This is C# Library for C++ from Joe");

        }


        public void setInfo(String _name, int _age, String _phoneNum)

        {

            name = _name;

            age = _age;

            phoneNum = _phoneNum;

        }

    }

}



4) C# 소스코드 빌드하기

Visual Studio 2017의 메뉴에서 '빌드' - '해당프로젝트명 빌드'를 클릭하여 소스 코드를 빌드한다.

(아래 이미지의 경우는 프로젝트 명이 ExMakeClassLibSerialRead라고 가정할 경우이다)



C#의 프로젝트명\bin\Debug\해당프로젝트명.dll 이 생성이 되어 있을 것이다.



5) 빌드된 C#의 .dll을 레지스트리 등록 및 .tlb 파일 만들기

.tlb 파일을 만드는 이유는 C#은 .NET 기반이기 때문에 이를 C++에서 사용가능하도록 하기 위해서이다.


"Developer Command Prompt for VS 2017"창을 관리자 권한으로 열고 C#의 해당 .dll이 있는 경로로 이동한다.

"Developer Command Prompt for VS 2017"창을 열어야 regasm을 정상적으로 이용할수 있다.

여는 방법은 윈도우즈의 시작 - Visual Studio 2017 하위 항목 열기 - Developer Command Prompt for VS 2017 메뉴 위에서 마우스 우측 클릭 - 관리자 권한으로 실행한다.

(만일 DOS 창을 관리자 권한으로 열어서 사용할 경우는 regasm.exe를 C#의 .dll이 있는 폴더로 복사해서 아래 명령을 처리하면된다)


C#용 .dll이 있는 디렉토리로 이동해서 다음 명령을 한다. 

아래에서 ExMakeClassLib는 프로젝트 명이다.


regasm ExMakeClassLib.dll /tlb:ExMakeClassLib.tlb


그러면 다음과 같은 메시지가 보이면 성공한 것이다.


Microsoft .NET Framework 버전 4.7.2556.0용

Microsoft .NET Framework Assembly Registration Utility 버전 4.7.2556.0

Copyright (C) Microsoft Corporation.  All rights reserved.


형식이 등록되었습니다.

어셈블리를 'D:\Joe\CSharp\ExMakeClassLib\ExMakeClassLib\bin\Debug\ExMakeClassLib.tlb'(으)로 내보내고 형식 라이브러리를 등록했습니다.


.dll과 .tlb 생성은 C# 소스 코드를 수정할 때마다 항상 같이 만들어 주어야 한다.

이렇게 만들어진 .dll과 .tlb를 C++의 프로젝트가 있는 디렉토리와 C++의 실행파일이 있는 위치에 복사해 준다.

이렇게 만들어진 C#용 library를 C++(혹은 MFC)에서 어떻게 사용하는지는 아래 링크에서 확인할수 있다.

(C++에서 C# .dll 라이브러리 사용하는 법)



Visual Studio 2017에서 C#으로 프로그래밍 중 "참조" - "참조 추가" 작업시 아래와 같은 에러 발생했을 경우에 대한 해법이다.


"작업을 완료할수 없습니다. 해당 인터페이스를 지원하지 않습니다."

(The operation could not be completed, no such interface supported)



아래 경로로 이동한다.

cd C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\PublicAssemblies


원하는 것은 Microsoft.VisualStudio.Shell.Interop.11.0.dll 파일이 있는 곳으로 이동해야 한다.


다음 명령을 실행한다.


C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\PublicAssemblies>gacutil -i Microsoft.VisualStudio.Shell.Interop.11.0.dll


그러면 다음과 같은 결과가 나타나면 문제가 해결 된 것이다. 


『  Microsoft (R) .NET Global Assembly Cache Utility.  Version 3.5.30729.1

    Copyright (c) Microsoft Corporation.  All rights reserved.


   Assembly successfully added to the cache  』


Visual Studio 2017을 다시 실행한다.


그런데 gacutil.exe가 해당 경로에는 없다. 그런데 gacutil.exe가 여러 군데, 여러 버전이 있는 것 같다. 파일 사이즈가 상이한 동일 이름의 gacutil.exe가 여러 군데 있다. 내 경우는 아래 경로에 있는 gacutil.exe로 문제 해결되었다.


C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\gacutil.exe


자세한 내용은 아래 사이트 참조

https://developercommunity.visualstudio.com/content/problem/75105/cant-add-reference-error-pops-up-no-such-interface.html




"Start Thread" 버튼 클릭시 0.5초 간격으로 1부터 순차적으로 숫자를 출력하고 

"Stop Thread" 버튼 클릭시 쓰레드를 종료하는 초 간단 C#용 Thread 예제 코드이다.

화면 구성은 다음과 같다.


using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Threading;

using System.Threading.Tasks;

using System.Windows.Forms;


namespace ExSerialRead

{

    public partial class Form1 : Form

    {

        private int cnt = 0;

        private Thread rTh;


        public Form1()

        {

            InitializeComponent();

        }


        private void btnStartThread_Click(object sender, EventArgs e)

        {

            //Thread 객체 생성. 생성시 이 Thread가 실행할 메소드를 지정해 준다. 

            //여기서는 Count라는 메소드를 rTh라는 쓰레드가 실행하게 될 것이다.

            rTh = new Thread(Count);

            rTh.Start();

        }


        // Thread로 수행할 작업

        private void Count()

        {

            while(true)

            {

                ++cnt;

                Thread.Sleep(500);

                //Console.WriteLine("crr cnt : " + cnt); 

                Console.WriteLine("crr cnt is {0} ", cnt); 


                if (cnt > 200)

                {

                    rTh.Abort(); //쓰레드 강제 종료


                    Console.WriteLine("Thread 종료. cnt : " + cnt);

                    break;

                }

            }

        }


        private void btnStopThread_Click(object sender, EventArgs e)

        {

            rTh.Abort(); //쓰레드 강제 종료

            Console.WriteLine("Thread 종료~");

        }

    }

}





C#에서 실시간 그래프를 그리다 보면 그리기 화면이 깜빡이는 현상을 볼수 있다.

이 문제를 해결하기 위해서는 그리기 할 대상을 double buffering 처리를 해 주면 깔끔하게 해결이 된다.

보통은 System.Windows.Forms::Panel위에 그리기 작업을 하게 될텐데, 혹은 Bitmap에 먼저 그린 후 그 Bitmap을 Panle에 그리는 방식이거나...

아무튼 이 경우 Panel을 double buffering 처리를 해 주면 된다.

문제는 Form은 화면 디자인 시점에 DoubleBuffered라는 속성 값을 true로 해 주면 되지만 Panel의 경우는 double buffering을 설정하는 메소드나 속성(property)이 막바로 접근이 안된다.

즉 protected 메소드이고 protected 속성이다. 


MSDN을 보면 속성의 정의가 아래와 같이 protected이고

protected virtual bool DoubleBuffered { get; set; }


메소드도 아래와 같이 protected이다.

protected virtual bool DoubleBuffered { get; set; }


따라서 Panel에 double buffering을 설정할려면 Panel을 상속받은 사용자 정의 클래스를 만들어서 이 사용자 정의 Panel을 사용해야 가능하다.


(1) Visual Studio의 해당 프로젝트명에서 마우스 우측 클릭 ⇒ 추가(Add) ⇒ New Item(새 항목) ⇒ Class를 선택 후 Class 이름 지정


(2) 아래의 내용을 추가한다.

    class DoubleBufferPanel : Panel

    {

        public DoubleBufferPanel()

        {

            this.SetStyle(ControlStyles.OptimizedDoubleBuffer | 

   ControlStyles.UserPaint |

                                ControlStyles.AllPaintingInWmPaint, true

                );


            this.UpdateStyles();

        } 

    }


(3) Form1.Designer.cs에서 기존의 Panel을 사용자가 정의한 Panel로 변경한다.




위의 적색 사각형 영역 (Windows Form Designer generated code 영역)을 더블 클릭하여 InitializeComponent() 안에 있는


this.panel1 = new System.Windows.Forms.Panel();

this.panel1 = new Bitmap_Panel.DoubleBufferPanel();

와 같이 사용자 정의 Panel로 변경해 준다.


이후 부터 화면이 깜빡이는 현상 없이 잘 처리 될 것이다.


MSDN이 소개하는 DoubleBuffered에 대한 설명이다.


Gets or sets a value indicating whether this control should redraw its surface using a secondary buffer to reduce or prevent flicker.





Visual Studio 2015 Community 버전 사용시 종종 보여지는 문제 중 하나가 새로운 Project를 열때(File - 새로 만들기 - 프로젝트) 

아래와 같은 템플릿이 보여야 되는데 대부분이 보이지 않고 Silverlight나 WPF 정도만 보이는 경우가 있다.

혹은 ASP.NET 웹 응용 프로그램 템플릿이 보이지 않는다거나...




정작 Windows Forms 응용 프로그램을 개발할 템플릿이 없어 난감한 경우를 만난다.

이 문제에 대해 Stack Overflow 등 여러 해법이 있어나 잘 작동 안되는 경우들이 있다.


가장 손쉬운 해법은


 ① Visual Studio 2015가 설치된 다음 폴더(<설치폴더>\Common7\IDE)로 이동한다. 만일 아래 경로에 설치되어 있다면 

     C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE

 ② 위의 폴더에서 projecttemplatecache와 itemtemplatecache 폴더 2개를 삭제한다. 이 2개의 캐시 폴더를 삭제하지 않고서는 devenv /installvstemplates 명령어가 효과가 없다.


 ③ 위의 폴더에서 커맨더 창(DOS 창)을 띄워 다음 명령어를 차례로 실행한다.

     devenv /installvstemplates.

     devenv /ResetSettings.


또 하나의 해법은 다시 설치를 통해 "수정(Modify)"을 선택하여 진행하다 보면 원하는 기능이 체크 해제 되어 있는 것을 발견하게 될 것이다.

이를 체크해서 다시 설치를 함을 통해 문제를 해결할수도 있다.


이 문제에 대해 아래 사이트 참조.


https://social.msdn.microsoft.com/Forums/vstudio/en-US/0dd15332-7744-44ea-85a8-5ec6b36febcc/visual-studio-2015-missing-project-templates?forum=visualstudiogeneral


http://stackoverflow.com/questions/32613505/missing-mvc-template-in-visual-studio-2015



+ Recent posts