NAS에 MariaDB(MySQL)를 설치는 했는데..어떻게 사용해야 할까요? (약 1시간 정도 고민..) 

역시 가장 만만한 RESTfull 서비스를 만들어서 호출하면 되겠죠? 그렇다면, Azure에서 RESTfull 서비스를 만들어서 CRUD를 해 볼까요?


MySQL과 연결하는 서비스를 만드는 것이 저도 처음이라 엄청난 삽질을 했습니다. ㅋㅋ 간단하지는 않네요.



0. 참고

NAS에 Database를?

NorthWind Database 생성

Visual Studio를 사용하여 Azure App Service에서 웹앱 문제 해결

ERROR 1044 (42000): Access denied for user ''@'localhost' to database 'db'

Connect C# to MySQL



1. MySQL에 NorthWind Database 생성


참고에 있는 NorthWind Database 생성 페이지에서 Northwind.MySQL5.sql을 다운로드 합니다.


SQLyog 프로그램을 실행해서 DB에 접속하고, 다운로드받은 쿼리를 불러온 후 실행하면, northwind db가 생성됩니다.




2. 셈플 프로젝트 생성


Visual Studio를 실행하고 New -> Project를 선택하고 다음과 같이 선택합니다.



ASP.NET Web Application을 선택합니다. 여기서 해당 내용들이 나오지 않는다면, 비주얼 스튜디오를 설치할 때 웹 관련 항목을 선택하지 않았기 때문입니다. 그리고, Azure와 관련된 최신 업데이트를 완료하세요.


OK를 입력하여 다음 화면으로 넘어 갑니다.



여기서 Azure API App을 선택하고, OK를 클릭합니다.


프로젝트 생성 완료!



3. MySQL 사용을 위한 준비


프로젝트에서 오른쪽 버튼을 눌러 NuGet packages를 선택합니다.


Browse 탭에서 MySQL을 검색해서 MySQL.Data를 선택하고 설치합니다.




4. MySQLHelper


DB에 접속해서 쿼리를 실행시키는 부분을 헬퍼 클래스로 분리 합니다.


    /// <summary>
    ///     MySQL 헬퍼 클래스
    /// </summary>
    public class MySQLHelper
    {
        private static MySQLHelper _instance;

        private MySqlConnection _connection;

        /// <summary>
        ///     생성자
        /// </summary>
        public MySQLHelper()
        {
            Initialize();
        }

        /// <summary>
        ///     인스턴스
        /// </summary>
        public static MySQLHelper Instance => _instance ?? (_instance = new MySQLHelper());

        /// <summary>
        ///     초기화
        /// </summary>
        private void Initialize()
        {
            var server = "NAS 외부접속 아이피";
            var database = "northwind";
            var uid = "kakiadmin";
            var password = "비밀번호";
            var connectionString =
                $"SERVER={server};DATABASE={database};UID={uid};Pwd={password};SslMode=none;";

            _connection = new MySqlConnection(connectionString);
        }

        /// <summary>
        ///     연결
        /// </summary>
        /// <returns></returns>
        private Tuple<bool, string> OpenConnection()
        {
            try
            {
                _connection.Open();
                return new Tuple<bool, string>(true, null);
            }
            catch (MySqlException ex)
            {
                //When handling errors, you can your application's response based
                //on the error number.
                //The two most common error numbers when connecting are as follows:
                //0: Cannot connect to server.
                //1045: Invalid user name and/or password.
                switch (ex.Number)
                {
                    case 0:
                        Debug.WriteLine("Cannot connect to server.  Contact administrator");
                        break;

                    case 1045:
                        Debug.WriteLine("Invalid username/password, please try again");
                        break;
                }

                return new Tuple<bool, string>(false, ex.Message);
            }
        }

        /// <summary>
        ///     연결 종료
        /// </summary>
        /// <returns></returns>
        private bool CloseConnection()
        {
            try
            {
                _connection.Close();
                return true;
            }
            catch (MySqlException ex)
            {
                Debug.WriteLine(ex.Message);
                return false;
            }
        }

        /// <summary>
        ///     연결 테스트
        /// </summary>
        /// <returns></returns>
        public async Task<string> GetConnectTestAsync()
        {
            var ds = new DataSet();
            var query = "SELECT * FROM Employees";
            var result = OpenConnection();
            if (result.Item1 == false) return result.Item2;
            var adpt = new MySqlDataAdapter(query, _connection);
            //var cmd = new MySqlCommand(query, _connection);
            await adpt.FillAsync(ds);
            CloseConnection();
            return "success connecting";
        }

        /// <summary>
        ///     결과 반환하지 않는 쿼리 실행, Insert, Update, Delete
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public async Task<string> ExecuteNonQueryAsync(string query)
        {
            if (string.IsNullOrEmpty(query)) return "Empty strings are not supported.";
            var result = OpenConnection();
            if (result.Item1 == false) return result.Item2;
            var cmd = new MySqlCommand(query, _connection);
            var resultExecute = await cmd.ExecuteNonQueryAsync();
            CloseConnection();
            return resultExecute.ToString();
        }

        /// <summary>
        ///     결과 반환하는 쿼리 실행 Select
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public async Task<DataSet> ExecuteAsync(string query)
        {
            if (string.IsNullOrEmpty(query)) return null;
            var ds = new DataSet();
            var result = OpenConnection();
            if (result.Item1 == false) return null;
            var adpt = new MySqlDataAdapter(query, _connection);
            await adpt.FillAsync(ds);
            CloseConnection();
            return ds;
        }
    }


Initialize()에 접속 아이디와 비밀번호는 지난번 포스트에서 이야기 했듯이 telnet으로 연결해서 직접 추가를 해주어야 합니다.


mysql에 root 아이디로 접속


$ mysql -uroot -p


kakiadmin 아이디 추가


mysql> CREATE USER 'kakiadmin'@'192.168.35.1' IDENTIFIED BY 'your_password';
mysql
> grant all privileges on *.* to 'kakiadmin'@'192.168.35.1';


여기서 주의할 사항은 아이디를 추가할 때 뒤에 붙이는 아이피는 집에서 테스트를 할 때는 게이트웨이 주소를 넣으셔야 하며, 차후 Azure에 서비스를 올리면 Azure 서비스에서 사용하는 아이피를 넣어야 한다는 것입니다. 


동일한 이름을 가지고 있어서 호출하는 Ip에 맞춰서 아이디가 각각 존재해야 한다는 점입니다.


어떤 아이피를 가지는 아이디를 추가해야하는지 확인하기 위해서 GetConnectTest 메소드를 호출하면 됩니다.



5. ValuesController.cs 수정


        /// <summary>
        ///     연결 테스트
        /// </summary>
        /// <returns></returns>
        [Route("~/getconnecttest")]
        public async Task<string> GetConnectTest()
        {
            return await MySQLHelper.Instance.GetConnectTestAsync();
        }


위의 코드를 추가한 후 실행하면 브라우저에서는 HTTP Error 403.14 - Forbidden 이런 오류가 출력되는데 무시하고, localhost:12267을 복사한 후 postman을 이용해서 테스트를 해봅니다.


정상적으로 연결이 완료되고, 데이터를 조회했다면 다음과 같은 결과가 반환됩니다.

실패했을 경우 1045 오류가 발생하면서, 메시지가 나오는데 그 곳에 나오는 아이피를 이용해서 아이디를 추가해주시면 됩니다.


여기서 제가 실패하지 않는 이유는 이미 로컬 아이피를 이용해서 계정을 등록해 놓았기 때문입니다. 처음하시는 분들은 여기서 반드시 오류가 발생합니다. 9번 항목을 참고해서 처리하세요



6. 데이터 조회


        // GET api/values
        [SwaggerOperation("GetAll")]
        public async Task<DataSet> Get()
        {
            return await MySQLHelper.Instance.ExecuteAsync("SELECT * FROM Products");
        }


ValuesController.cs에 Get메소드를 위와 같이 수정하고 실행 합니다.


postman에서 http://localhost:12267/api/values를 호출하면 결과가 반환 됩니다.


DataSet을 Json형태로 변환해서 반환합니다.

물론 여기서는 쿼리를 미리 입력해서 처리했지만, request로 받아서 처리할수도 있겠죠? 쿼리를 직접 받기 위해서는 Post를 이용해야 합니다.



7. 쿼리 직접 입력 받아서 실행하기


        // POST api/values
        [SwaggerOperation("Create")]
        [SwaggerResponse(HttpStatusCode.Created)]
        public async void Post([FromBody]JObject value)
        {
            var query = value["value"].ToString();
            var result = await MySQLHelper.Instance.ExecuteNonQueryAsync(query);
        }


ValueController.cs를 위와 같이 수정합니다. 특히 string을 JObject로 수정하는 것을 잊으면 않됩니다.


수정 완료 후 postman에서 아래와 같이 호출합니다.


post를 선택하고, http://localhost:12267/api/values를 입력합니다.

headers에 content-type은 x-www-form-urlencoded로 지정되어 있어야 하며, Body에 x-www-form-urlencoded를 선택한 후 value, INSERT Employees(LastName, FirstName) VALUES('Park','MunChan'); 를 각각 입력 후 send를 누릅니다.


물론 반환되는 결과는 없습니다. 호출후 db를 조회해보면 Employees 테이블에 레코드가 하나 추가되어있는 것을 볼 수 있습니다.



8. 서비스를 Azure에 올리기


프로젝트에서 마우스 오른쪽 -> Publish를 선택하면 아래와 같은 화면이 나옵니다. Create New에서 Publish를 눌러줍니다.



Create App Service에서 App 이름을 지정하고, 서브스크립션을 선택합니다. 비주얼 스튜디오 로그인된 계정으로 가입된 Azure 계정을 찾는것이 기본이며, 다른 계정을 선택하실 수도 있습니다. 


MVP는 MSDN 서브스크립션을 가지고 있기 때문에 선택했습니다. 그외 Resource Group, Hosting Plan등은 사용자가 임으로 만들어서 사용하시면 됩니다.


Create 버튼을 눌러서 서비스를 생성하면 됩니다.



배포가 완료되면 



완료화면이 출력됩니다. 이제 서비스를 호출해서 테스트 합니다.



9. 마무리 작업


postman으로 http://mysqlsample2018.azurewebsites.net/getconnecttest 주소를 get 호출하면


"Authentication to host 'xx.xx.xx.xx' for user 'kakiadmin' using method 'mysql_native_password' failed with message: Access denied for user 'kakiadmin'@'52.231.xx.xxx' (using password: YES)"


위와 같은 오류가 발생합니다. 즉, kakiadmin으로 MySQL 에 로그인을 하지 못했다는 내용입니다. 그런데 찾는 계정이 'kakiadmin'@'52.231.xx.xxx' 입니다. 이런 경우 해당 계정을 MySQL 에 추가해 주어야만 로그인이 가능합니다.


추가!!

뒤에 ip부분을 생략하고 싶으면 kakiadmin@% 이렇게 만들면 어떤 아이피든지 통과가 가능합니다. ㅜㅜ



4번에 있는 방법을 이용해서 계정을 추가해 주면 완료 가능합니다.


아이디를 추가한 후 다시 호출해보니 success connecting이라는 메시지가 출력되었습니다.


마지막으로 조회를 테스트해보니 정상적으로 조회되는 것을 알 수 있습니다.

최종 소스에서 조회하는 테이블을 Employees로 변경했습니다.


http://mysqlsample2018.azurewebsites.net/api/values


{
    "Table": [
        {
            "ProductID": 1,
            "ProductName": "Chai",
            "SupplierID": 1,
            "CategoryID": 1,
            "QuantityPerUnit": "10 boxes x 20 bags",
            "UnitPrice": 18,
            "UnitsInStock": 39,
            "UnitsOnOrder": 0,
            "ReorderLevel": 10,
            "Discontinued": 0
        },
        {
            "ProductID": 2,
            "ProductName": "Chang",
            "SupplierID": 1,
            "CategoryID": 1,
            "QuantityPerUnit": "24 - 12 oz bottles",
            "UnitPrice": 19,
            "UnitsInStock": 17,
            "UnitsOnOrder": 40,
            "ReorderLevel": 25,
            "Discontinued": 0
        },

.....



10. Azure debug


에저에 올라간 서비스의 에러를 확인하기 위해서는 참고에 있는 내용을 참고하시면 될 것 같습니다.



11. 소스

https://github.com/kaki104/ConnectingAzureToMySQL


블로그 이미지

kaki104

/// Microsoft MVP - Windows Development 2014 ~ 2019 5ring /// twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

집에 NAS가 하나 있습니다. 모두 아시는 내용이죠?? 이 녀석 사용하지 좀 오래 된 듯한데.. 하여간 지금 시놀로지(헤놀로지) 5.2-5644 Update 5 버전이 설치가 되어있고, 영화, 음악, 사진 등을 잘 넣어서 보고있는데.. 



오늘 아침에 갑지가 이 녀석에게 DB 설치가 가능할 것 같다는 느낌이 딱! ... 결국 3시간 만에 성공을 한 것 같습니다.



0. 참고


https://m.blog.naver.com/PostView.nhn?blogId=oioi9i&logNo=220408830865&proxyReferer=https%3A%2F%2Fwww.google.co.kr%2F

http://zetawiki.com/wiki/MySQL_원격_접속_허용



1. 설치는 쉬웠다!



피키지 센터에 MariaDB가 있어서 찾아보니 과거 MySQL의 친구(?) 버전이라 호화성이나 성능이 괜찮을 것 같아서 설치하기로 하고, Install 버튼을 누르니 특별히 뭐 물어보는 것도 없이 바로 설치 완료..


으흠;;



2. Admin계정 비밀번호 등은??



물개를 클릭하면 간단한 윈도우가 하나 뜨는데..



Change MariaDB password 버튼을 클릭해서, root 계정의 비밀번호를 변경할 수 있습니다. root가 admin인 것 같습니다..아마도..



3. DB설치는 되었는데, 어떻게 접속을 해야하지??


마리아 DB TX Tools 폴더에 오면 Tools 다운로드가 지원됩니다. 

https://mariadb.com/downloads/mariadb-tx/tools 


SQLyog를 선택하고 Download 버튼을 클릭하면, 응용프로그램이 다운 됩니다. 14일간 트라이얼로 사용할 수 있는 버전으로 정식으로는 99불을 주고 사면 됩니다.


물론 MySQL과 동일하기 때문에 MySQL 관리가 가능한 다른 프로그램을 사용하셔도 상관 없습니다.



4. DB 접속 1차 시도


처음 시도는 역시 실패! 실패 이유는 DB가 외부접속을 허용하지 않기 때문입니다.


DB를 외부접속 가능하게 하려면.. 3개의 DB 커맨드를 날려야 하는데..


이 커맨드를 날릴려면 NAS에 Telnet으로 접속을 해야 합니다.



5. NAS에 Telnet으로 접속하기



NAS에 Telnet으로 접속하기 위해서는 Control Panel에서 Terminal & SNMP를 선택해서 Enable Telnet service, Enable SSH service 2개를 체크 해 주고, Apply를 클릭합니다.


이제 Telnet을 이용해서 NAS에 접속 할 수 있습니다. 음음..그런데 윈도에 기본적으로 Telnet 클라이언트 설치가 되어 있지 않으니 설치를 해야겠죠?


Telnet 클라이언트 설치하는 부분에 대해서는 설명을 생략하겠습니다.



6. Telnet 클라이언트로 NAS 접속, MariaDB 접속


윈도우에서 Windows PowerShell을 실행 하고, 다음의 명령을 입력해서 NAS에 연결합니다. 아이피 주소는 자신의 NAS IP 주소입니다.


telnet 192.168.35.94



아이디와 패스워드를 입력해야 하는데..원래 사용하던 계정으로 로그인이 앙되더라구요..그래서 admin 계정을 사용할 수 있게 변경해야합니다.(원래 사용하던 계정을 사용해서 로그인할 수 있으면 그걸로 그냥 하셔도 될 듯..)



User -> admin -> Disallow the user to change account password, Disable this account 2가지의 체크를 풀고, OK를 입력합니다. 


BusyBox v1.16.1 (2015-11-12 18:06:25 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.


kaki_server>


로그인이 완료되면 대충 이런 메시지가 출력됩니다. 후후


이제 MariaDB에 연결해서 쿼리를 날려줘야 하는데. db에 연결하는 명령어를 몰라서 찾아보니..


kaki_server> mysql -u root -p

Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 4
Server version: 5.5.47-MariaDB Source distribution


Copyright (c) 2000, 2015, Oracle, MariaDB Corporation Ab and others.


Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.


MariaDB [(none)]>


password는 아까 변경해준 녀석을 입력하시면 될 것 같습니다. 아마도..



7. 외부 접속 가능하도록 쿼리 입력


MariaDB [(none)]> INSERT INTO mysql.user (host,user,authentication_string,ssl_cipher, x509_issuer, x509_subject) VALUES ('%','root',password('패스워드'),'','','');
Query OK, 1 row affected (0.00 sec)


MariaDB [(none)]> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%';
Query OK, 0 rows affected (0.00 sec)


MariaDB [(none)]> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)


굵은 글씨 부분을 차례대로 입력하시고, 패스워드는 패스워드를 입력해 주세요, 


여기까지 하시면 외부에서 db에 연결 할 수 있습니다.



8. DB 접속 2차 시도..


4번으로 돌아가서 비밀번호를 입력하고 connect를 입력하면~



짜잔~ 연결이 되었습니다.



9. 주의사항


telnet 접속이 가능하도록 열어 놓고 작업을 한 후 바로 닫아 주어야 합니다. 그렇지 않으면 무지막지한 공격이 들어옵니다..쿨럭..


블로그 이미지

kaki104

/// Microsoft MVP - Windows Development 2014 ~ 2019 5ring /// twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

Tag mariaDB, mysql, Nas



이번 포스트 주제는 Windows 10 IoT Core - Hello World 입니다.

IoT를 처음 시작하시려는 분들을 위한 포스트로, 자세하게 설명하고 있습니다. 앞으로 동영상의 방향은 IoT를 이용한 여러가지 앱을 만들어서 응용하는 방향으로 진행할 예정입니다.


많은 구독 신청 부탁드립니다.



Part1


*Why Windows 10 IoT Core?
Windows 10 IoT Core Compatible Boards
Raspberry Pi 3 kits
What can you make?
라즈베리파이에 윈도우 설치 및 설정
해상도 설정 참고 자료





Part2


Hello World Windows 10 IoT Core




PPT


iot-helloworld.pptx


블로그 이미지

kaki104

/// Microsoft MVP - Windows Development 2014 ~ 2019 5ring /// twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/



이번 포스트는 GitHub와 VS를 이용해서 어떻게 프로젝트를 관리하는가에 대한 내용입니다.

5개의 part로 구분되어 있으며, 각 part별 내용은 바로 상단에 있으니 필요한 내용을 바로 찾아서 보실 수 있을 것 같습니다.

앞으로 GitHub를 이용해서 오픈 소스 프로젝트를 진행하기 위한 준비 단계이니 꼭~ 참고해 주세요


혹시 내용 중 수정 사항은 이곳에 리플로 남겨주시거나 메일로 알려주시면 수정하도록 하겠습니다.


ps. diablo3hub앱은 빌드 후 실행 가능하게 수정하도록 하겠습니다.

-> 수정 완료했습니다.


Part1
환경 및 준비
참고 자료
What is GitHub?
Why git for developer
GitHub 가입




Part2
GitHub Flow - Create a branch
GitHub Flow – Add commits
GitHub Flow – Open a Pull Request
GitHub Flow – Discuss and review your code
GitHub Flow – Deploy
GitHub Flow – Merge




Part3
GitHub – New repository
GitHub – Project 추가
GitHub – Extension download and install
UWP 프로젝트 추가, 커밋, 싱크




Part4
GitHub – Collaborators 추가
GitHub – Clone repository
GitHub – VS와 연동 작업




Part5
GitHub - Fork 후 작업하기




PPT


GitHub와VS연동작업.pptx


블로그 이미지

kaki104

/// Microsoft MVP - Windows Development 2014 ~ 2019 5ring /// twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/


1. VSTS!


Visual Studio Team Service의 약자로 예전에 Team Foundation Service(TFS)의 변경된 이름입니다.



2. 내용


. 프로젝트 팀원 추가

. Assign user

. 권한 관리

. Wiki 문서 작성

. Pull Request

. Work item link


등의 내용을 다루고 있습니다.





3. 파일


TFS안내part1-2.pptx


블로그 이미지

kaki104

/// Microsoft MVP - Windows Development 2014 ~ 2019 5ring /// twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

Tag git, VSTS


1. VSTS ?
Visual Studio Team Service의 약자로 예전에 Team Foundation Service(TFS)의 변경된 이름입니다.



2. 동영상 제작 동기
판교에서 근무할 때 스크럼(애자일 개발 프로세스)를 처음 접하고, jira와 bitbucket을 사용했었는데, VSTS를 이용해도 동일한 작업이 가능함을 알려드리기 위해서 만들었습니다.



3. 목표
개인 혹은 소규모 단위 프로젝트를 개발 할 때 VSTS의 스크럼(애자일 개발 프로세스)를 이용해서 효율적인 개발을 진행하는 것 입니다.


https://youtu.be/Y-XC-d20-Rg




4. 파일 

파일명은 part1이지만, part2 파일을 만들지는 않을 듯 합니다.~

TFS안내part1.pptx



블로그 이미지

kaki104

/// Microsoft MVP - Windows Development 2014 ~ 2019 5ring /// twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

페북에 올라온 내용을 확인하기 위해서 앱을 열어보니 동일한 현상이 발생합니다.


https://social.msdn.microsoft.com/Forums/vstudio/en-US/d5ca935d-cec3-4a3b-9c22-a452c6e7a0f3/visual-statesblendwindows-10-fall-creators-updatewindows-visual-states-and-data-tabs?forum=visualstudiogeneral


이 문제에 관해서 질문은 올라왔는데..해결 방법은 아직 없네요..


그래서, 일단 아래 화면처럼 버전을 변경합니다.

빌드를하고,, 비주얼스튜디오를 종료 합니다.




다시 프로젝트를 열면 자동으로 load가 않되는데.. 그걸 마우스 오른쪽 클릭해서 Reload project를 선택해서 불러 옵니다.

그러면 아래 내용 처럼 사용이 가능합니다.




이 방법으로도 않된다면..좀 기다려야 할 것 같습니다. ㅜㅜ 


블로그 이미지

kaki104

/// Microsoft MVP - Windows Development 2014 ~ 2019 5ring /// twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

페이스북에 올라온 민원 해결을 위해서 간단하게 프로젝트를 만들었습니다.

Q. 텍스트박스에 숫자를 입력하면 자동으로 컴마를 찍어주고, 백스페이스를 누르면 삭제가 되도록 하고 싶습니다.~

A. 일단 여러분들이 의견 주셨습니다.

우선 컨버터를 이용한 방법이 이야기가 되어서, 저도 컨버터를 이용해서 처리를 할려고 해봤는데..

컨버터는 프로퍼티 체인지 이벤트가 발생했을 경우에 컨버터가 값을 변경해 주는 역할을 합니다...그런데.. TextBlock에는 뷰모델에서 변경된 내용을 바로 화면에 이쁘게 뿌려주는데.. 텍스트박스에서는 키가 입력되면, 그 내용을 바로 뷰모델에 값을 넣어주는 역할만을 하고, 프로퍼티가 변경된 내용을 화면에 다시 뿌려주지는 않습니다.

StringFormatConverter.cs

using System;
using Windows.UI.Xaml.Data;
namespace App1
{
    public class StringFormatConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            var format = parameter as string;
            if (!string.IsNullOrEmpty(format) && value is string)
            {
                int.TryParse(value.ToString(), out var number);
                return string.Format(format, number);
            }
            return value;
        }
        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            if (value == null) return null;
            int.TryParse(value.ToString().Replace(",", ""), out var number);
            return number.ToString();
        }
    }
}

음..그래서 결국 비헤이비어를 만들어서 처리를 해보았습니다.

NumberTextBoxBehavior.cs

using Windows.System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Microsoft.Xaml.Interactivity;
namespace App1
{
    public class NumberTextBoxBehavior : Behavior<TextBox>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.KeyUp += AssociatedObject_KeyUp;
        }
        private void AssociatedObject_KeyUp(object sender, KeyRoutedEventArgs e)
        {
            switch (e.Key)
            {
                case VirtualKey.Number0:
                case VirtualKey.Number1:
                case VirtualKey.Number2:
                case VirtualKey.Number3:
                case VirtualKey.Number4:
                case VirtualKey.Number5:
                case VirtualKey.Number6:
                case VirtualKey.Number7:
                case VirtualKey.Number8:
                case VirtualKey.Number9:
                case VirtualKey.Back:
                    var numberText = AssociatedObject.Text;
                    if (string.IsNullOrEmpty(numberText)) return;
                    var number = int.Parse(numberText.Replace(",", ""));
                    var formatString = string.Format("{0:N0}", number);
                    if (formatString != number.ToString())
                    {
                        AssociatedObject.Text = formatString;
                        AssociatedObject.SelectionStart = AssociatedObject.Text.Length;
                    }
                    break;
            }
        }
        protected override void OnDetaching()
        {
            AssociatedObject.KeyUp -= AssociatedObject_KeyUp;
            base.OnDetaching();
        }
    }
}


MainPage.xaml


<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App1"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:Custom="using:Microsoft.Xaml.Interactivity"
    x:Class="App1.MainPage"
    xmlns:converters="using:Microsoft.Toolkit.Uwp.UI.Converters"
    mc:Ignorable="d"
    x:Name="mainPage">
    <Page.Resources>
        <local:StringFormatConverter x:Key="StringFormatConverter" />
    </Page.Resources>
    <Page.DataContext>
        <local:MainPageViewModel />
    </Page.DataContext>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
            <TextBox MinWidth="200" TextAlignment="Right"
                     Text="{Binding Number, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                <Custom:Interaction.Behaviors>
                    <local:NumberTextBoxBehavior />
                </Custom:Interaction.Behaviors>
            </TextBox>

            <TextBlock Text="{Binding Number}" />
            <Button Click="ButtonBase_OnClick" />
        </StackPanel>
    </Grid>
</Page>


결과 화면입니다.



파일

NumberTextBox.zip


블로그 이미지

kaki104

/// Microsoft MVP - Windows Development 2014 ~ 2019 5ring /// twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

Tag Behavior, UWP

저녁마다 앱 만든다고 뚝딱뚝딱하고 있다가, Web Service에서도 사용해야하는 데이터가 있어서, PCL(Portable Class Library)를 추가하려고 했는데.. 않되더군요..쿨럭 이게 무슨일인가하고 내용을 잘 살펴보니 PCL은 Visual Studio 2013까지만 지원을하고 Visual Studio 2017에서는 .Net Standard를 사용하라고 나와있더군요..그래서, 공부도 할겸 포스팅을 하기로 했습니다.



1. .Net Standard가 뭔가요?


과거에 우리는 이렇게 복잡한 세상에서 개발을 진행 했습니다. 닷넷 프레임웍 따로, 닷넷 코어 따로, 자마린 따로..

클래스 라이브러리도 각각 만들어서 사용하고 있었죠..




이런 복잡한 구조를 개선하기 위해 닷넷 스텐다드가 추가되었습니다!


1) 닷넷 스탠다드는 코드를 공유하기 편리합니다.


과거 PCL을 이용해서 코드를 공유하던 것을 쉽게 대체할 수 있습니다.


2) 더 많은 API를 사용할 수 있습니다.


1.6에서 1.3k의 정도의 API를 사용할 수 있었지만, 2.0은 32k의 API 사용이 가능합니다.


3) 닷넷 프레임웍과 호환성이 좋습니다.


대부분의 NuGet package들이 닷넷 프레임웍을 대상으로 만들어져 있습니다. 그래서, 호환성을 높여서 닷넷 스텐다드 2.0을 이용할 경우 NuGet package의 70% 정도를 그대로 사용할 수 있습니다. 




위에서도 보았듯이 지원하는 플랫폼이 상당히 많습니다. 1.0이 나온것이 2016년이였는데(1.0 출시 안내).. 어느덧 2.0이..그동안 PCL만 사용했었는데..이제는 이 녀석을 이용해서 개발을 해야 할 것 같습니다.


.NET 표준 1.0 1.1 1.2 1.3 1.4 1.5 1.6 2.0
.NET Core 1.0 1.0 1.0 1.0 1.0 1.0 1.0 2.0
.NET Framework(.NET Core 1.x SDK 포함) 4.5 4.5 4.5.1 4.6 4.6.1 4.6.2
.NET Framework(.NET Core 2.0 SDK 포함) 4.5 4.5 4.5.1 4.6 4.6.1 4.6.1 4.6.1 4.6.1
Mono 4.6 4.6 4.6 4.6 4.6 4.6 4.6 5.4
Xamarin.iOS 10.0 10.0 10.0 10.0 10.0 10.0 10.0 10.14
Xamarin.Mac 3.0 3.0 3.0 3.0 3.0 3.0 3.0 3.8
Xamarin.Android 7.0 7.0 7.0 7.0 7.0 7.0 7.0 8.0

Universal Windows Platform (UWP)

10.0 10.0 10.0 10.0 10.0 16299 16299 16299
8.0 8.0 8.1
Windows Phone 8.1 8.1 8.1
Windows Phone Silverlight 8.0


UWP 지원에 대한 부분은 여기를 참고하세요. 

대충 아시겠죠? 이제 직접 사용해 보도록 하겠습니다.


닷넷 스탠다드 2.0에서 사용가능한 API 조회는 여기서~



2. .NET Standard 클래스 라이브러리 만들기


Windows Template Studio를 이용해서 Blank, Basic MVVM pattern 프로젝트를 생성합니다.


혹시 Windows Template Studio를 모르시는 분은 여기를 클릭하세요


이제 .Net Standard 프로젝트를 여기에 추가해줍니다.


솔루션에서 마우스 오른쪽 Add -> New Project...



.NET Standard선택 -> Class Libaray (.NET Standard) 선택 -> 이름 입력 후 OK



추가된 프로젝트 속성을 확인해보면 .NET Standard 2.0이라고 표시된 것을 확인할 수 있습니다.




3. UWP앱이랑 연결을 시키기..


UWP앱에서 마우스 오른쪽 -> Add -> Reference...선택



Projects -> NetStandard.Standard 프로젝트를 체크 하고 OK를 누르면 완료 입니다.



빌드를 해보도록 하죠.


이런..빌드를 했더니, 오류가 쫘르륵!! 위의 표에서 보면 이유를 알 수 있습니다. .NET Standard 2.0은 Windows 10 Fall Creators Update 버전만 지원합니다. 그래서, UWP 앱의 최소 지원 버전을 변경해 주어야 합니다.



UWP앱의 속성에 들어갓가서 아래와 같이 최소 버전을 Fall Creators Update로 변경해 주시고 다시 빌드를 하시면 완료됩니다.




4. DataSet 사용이 가능??


닷넷의 오래된 기능으로 메모리에 데이터를 저장하는 DataSet을 사용할 수 있었습니다. 그런데, 그동안 UWP에서는 지원이 앙되던 녀석이 였는데, .NET Standard 2.0에서 부활 했습니다.~


아래와 같이 TestClass를 만들어 보도록 하겠습니다.

* 아래 코드는 여기서 복사했습니다.


using System.Data;

namespace NetStandard.Standard.Helpers
{
    public static class TestClass
    {
        public static DataSet GetXmlFromDataSet()
        {
            // Create two DataTable instances.
            var table1 = new DataTable("patients");
            table1.Columns.Add("name");
            table1.Columns.Add("id");
            table1.Rows.Add("sam", 1);
            table1.Rows.Add("mark", 2);

            var table2 = new DataTable("medications");
            table2.Columns.Add("id");
            table2.Columns.Add("medication");
            table2.Rows.Add(1, "atenolol");
            table2.Rows.Add(2, "amoxicillin");

            // Create a DataSet and put both tables in it.
            var set = new DataSet("office");
            set.Tables.Add(table1);
            set.Tables.Add(table2);
           
            // Visualize DataSet.
            return set;
        }
    }
}




이 녀석을 UWP의 ViewModel에서 호출해서 사용해 보겠습니다.


리샤퍼를 사용하시는 경우에는 업데이트를 해야 인텔리 센스가 정상 동작하는 것 같습니다. 참고해주세요~

JetBrains ReSharper Ultimate 2017.2.2  Build 109.0.20171006.122324
ReSharper 2017.2.20171006.123800


using Windows.UI.Xaml.Navigation;
using NetStandardSample.Helpers;
using NetStandardSample.Services;
using NetStandard.Standard.Helpers;


namespace NetStandardSample.ViewModels
{
    public class MainViewModel : Observable
    {
        private string _xmlText;

        public MainViewModel()
        {
            NavigationService.Navigated += NavigationService_Navigated;
        }

        public string XmlText
        {
            get => _xmlText;
            set => Set(ref _xmlText, value);
        }

        private void NavigationService_Navigated(object sender, NavigationEventArgs e)
        {
            var set = TestClass.GetXmlFromDataSet();
            if (set == null) return;
            XmlText = set.GetXml();

        }
    }
}


MainPage.xaml


MainPage.xaml.cs에 


//public MainViewModel ViewModel { get; } = new MainViewModel(); 문장은 주석 처리했습니다.


<Page
    x:Class="NetStandardSample.Views.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:viewModels="using:NetStandardSample.ViewModels"
    Style="{StaticResource PageStyle}"
    mc:Ignorable="d">
    <Page.DataContext>
        <viewModels:MainViewModel />
    </Page.DataContext>

    <Grid
        x:Name="ContentArea"
        Margin="{StaticResource MediumLeftRightMargin}">

        <Grid.RowDefinitions>
            <RowDefinition x:Name="TitleRow" Height="48" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <TextBlock
            x:Name="TitlePage"
            x:Uid="Main_Title"
            Style="{StaticResource PageTitleStyle}" />

        <Grid
            Grid.Row="1"
            Background="{ThemeResource SystemControlPageBackgroundChromeLowBrush}">
            <TextBox Text="{Binding XmlText, Mode=TwoWay}" AcceptsReturn="True" />
        </Grid>
    </Grid>
</Page>


실행 결과는 아래와 같습니다. 정상적으로 DataSet을 반환 받아서, 메소드까지 실행해서 xml형태로 화면에 출력했습니다.


그렇다면.. 다른 프로젝트에서도 동일하게 동작할까용?



콘솔 프로젝트를 추가하고 사용해 보았습니다. 결과는 대만족입니다.




5. 소스


NetStandard.Sample.zip



PS. 그런데..Fall Creators Update UWP에서는 property marker가 제대로 동작하지 않는 버그가 있습니다. 신고는 했구용 다음번 업데이트 때 개선될 것으로 예상합니다. 흐흐


블로그 이미지

kaki104

/// Microsoft MVP - Windows Development 2014 ~ 2019 5ring /// twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

Part1

Part2

Part3

Part4

Part5

 

내급여 앱의 마지막 포스트 입니다. 이번에 다루는 내용은 두가지로, 블로그에 리플 남겨주신 분이 문의 주신 내용으로 한글 처리에 대한 내용과 내용을 파일로 저장하는 부분입니다.

 

 

1. 한글 처리..

 

원래 UWP 앱은 한글이 잘 표현 되는 데..이상하게 Windows Template Studio로 템플릿을 생성하면, 한글이 깨져서 나오네요..

 

저는 RadDataGrid에 들어간 내용만 그런 줄 알았는데, 일반 버튼에 들어간 한글도 바로 깨져서 약간 당황했습니다.

 

문제는 파일을 저장할 때 사용하는 코드 페이지 문제인데요.. 비주얼 스튜디오의 Blank 템플릿으로 UWP앱을 생성하고, MainPage.xaml을 연 후 file -> Save MainPage.xaml as ... -> Save 옆에 역 삼각형 클릭 -> Save With Encoding... 을 선택 -> Encoding 항목에 Unicode (UTF-8 with signature) - Codepage 65001이라고 되어 있습니다.

 

이상태에서 한글을 입력하고 실행을 해도 전혀 문제가 없죠, 그런데, Windows Template Studio의 탬플릿들은 코드 페이지가 Korean - Codepgae 949로 생성이 됩니다. 그래서, 실행을 하면 한글이 깨져서 출력됩니다.

 

 

 

* 해결 방법

 

1) 템플릿에서 만들어진 페이지들을 Save as 를 이용해서 Unicode (UTF-8 with signature) - Codepage 65001로 변경해 준다. 다행이 새 페이지를 만들때는 Unicode로 만들어 지고 있습니다.

 

2) 택스트를 Resource를 이용하도록 변경한다.

 

택스트의 다국어 지원을 위해, Resource 파일을 이용하도록 되어 있는데, 여기에 그냥 한글을 입력해 놓으면, 정상 적으로 한글이 출력됩니다. 물론, 제대로 만들기 위해서는 ko-kr 폴더를 생성하고, 그 아래 Resources.resw 파일을 넣어서 한글화를 하는 것이 가장 정상적인 방법입니다.

 

 

 

 

 

2. 저장 버튼 작업 마무리

 

Save CSV 버튼을 클릭했을 때 우리는 GridViewModel에 있는 SaveExcelCommand를 실행 하도록 바인딩을 걸어야 합니다.

 

앱바가 PivotPage.xaml에 있으니, 해당 페이지를 열고,

 

AppBarButton을 선택 -> Command 옆 네모 선택 -> Binding type : ElementName -> GridPage(gridPage는 x:Name) -> ViewModel -> SaveExcelCommand -> OK

 

 

그렇게 하면, 아래와 같은 바인딩이 생성 됩니다.

 

            <AppBarButton Icon="Save" Label="Save CSV" Command="{Binding ViewModel.SaveExcelCommand, ElementName=gridPage}" />

 

바인딩 자체는 이렇게 하면되는데... 문제는 버튼을 눌러도 뷰모델에서 아무런 반응이 없다는 것입니다. 예상 이유는 PivotPage가 생성 시점, GridPage 생성 시점이 서로 달라서 바인딩을 연결하지 못한 것이 아닌가 생각합니다.

 

과거에는 앱바와 뷰가 서로 따로 놀아서 바인딩을 할려면, StaticResource를 이용해야 했는데, 지금은 그정도는 아니지만, 다른 뷰의 뷰모델에 있는 커맨드를 바인딩으로 연결할 수는 없는 것 같습니다. 머, 찾아보면 좋은 해결책이 있을 것 같지만, 일단 여기서는 이렇게 바인딩을하지 않고, PivotViewModel에 커맨드를 실행 시키고, 그 커맨드에서 뷰모델의 커맨드를 실행 시키도록 코드를 작성 합니다.

 

 

PivotGrid.xaml

 

            <AppBarButton Icon="Save" Label="Save CSV" Command="{Binding SaveExcelCommand}" />

 

 

PivotViewModel.cs

 

        /// <summary>
        ///     기본 생성자
        /// </summary>
        public PivotViewModel()
        {
            PropertyChanged += (s, e) =>
            {
                if (e.PropertyName != nameof(SelectedView)
                    || SelectedView == null) return;
                ShowAppBar = SelectedView.Content is GridPage;
            };

            SaveExcelCommand = new RelayCommand(ExecuteSaveExcelCommand);
        }

 

        /// <summary>
        ///     엑셀 저장 커맨드
        /// </summary>
        public ICommand SaveExcelCommand { get; set; }

 

        private void ExecuteSaveExcelCommand()
        {
            var view = SelectedView?.Content as GridPage;
            view?.ViewModel.SaveExcelCommand.Execute(null);
        }

 

파일 저장 피커를 이용해서 폴더와 파일을 선택하고,

 

LINQ를 이용해서 저장할 데이터와 합계데이터를 CSV 형태 택스트로 만들고, 두개의 결과를 Union으로 합쳐서 최종 문자열로 만든 후 저장 합니다. 참! item.ToList()는 모델에 각 프로퍼티를 IList<object> 형태로 반환하도록 만들어 놓은 함수 입니다. 하지만, 좀더 신경을 써서 확장 메소드를 만들어 사용하는 것이 더 좋습니다.

 

옛날에는 catch {}에서 await를 사용할 수 없었는데, 이제는 사용이 가능합니다.

 

GridViewModel.cs

 

        private async void ExecuteSaveExcelCommand()
        {
            var savePicker = new FileSavePicker
            {
                SuggestedStartLocation = PickerLocationId.DocumentsLibrary
            };
            // Dropdown of file types the user can save the file as
            savePicker.FileTypeChoices.Add("CSV", new List<string> {".csv"});
            // Default file name if the user does not type one in or select a file to replace
            savePicker.SuggestedFileName = "MyPay" + BaseMonth;
            var result = await savePicker.PickSaveFileAsync();
            if (string.IsNullOrEmpty(result?.Name)) return;

            var saveDatas = from item in Works
                let itemText = string.Join(",", item.ToList())
                select itemText;

            var totalDatas = from item in TotalWorks
                let itemText = string.Join(",", item.ToList())
                select itemText;

            try
            {
                await FileIO.WriteTextAsync(result, string.Join("\n", saveDatas.Union(totalDatas)));
                await StaticCommonHelper.ShowMessageBoxAsync("작업을 완료 했습니다.");
            }
            catch (Exception e)
            {
                await StaticCommonHelper.ShowMessageBoxAsync(e.Message);
            }
        }

 

 

3. 한국어로 지정

 

생각해보니, 한국어로 지정을 하면, 입력 형식이나, 기본 포멧이 한국어로 변경되기 때문에 더 편한 것 같습니다. 아래 코드를 추가하면, 화면에 출력되는 내용들이 변경됩니다.

 

App.xaml.cs

 

        }
        {
            InitializeComponent();

            ApplicationLanguages.PrimaryLanguageOverride = "ko-KR";
           

            //Deferred execution until used. Check https://msdn.microsoft.com/library/dd642331(v=vs.110).aspx for further info on Lazy<T> class.
            _activationService = new Lazy<ActivationService>(CreateActivationService);
        }

 

 

 

4. 소스

 

몇가지 버그가 있기는 하지만, 간단하게 만들어 본 소스이니 개발하실 때 참고하시면 될 것 같습니다.

 

MyPay_part6.zip

 

'Windows App(Universal App) > Beginner' 카테고리의 다른 글

NumberTextBoxBehavior  (0) 2017.12.07
.Net Standard가 뭐에요?  (0) 2017.11.07
내급여 UWP 앱 개발 part6  (0) 2017.09.25
내급여 UWP 앱 개발 part5  (0) 2017.09.18
내급여 UWP 앱 개발 part4  (0) 2017.09.04
내급여 UWP 앱 개발 part3  (0) 2017.08.26
블로그 이미지

kaki104

/// Microsoft MVP - Windows Development 2014 ~ 2019 5ring /// twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

티스토리 툴바