이번 포스트 주제는 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 - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, 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 - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, 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 - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, 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 - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, 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 - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, 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 - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, 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 - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, 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 - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

내급여 UWP 앱 개발

Part1

Part2

Part3

Part4

Part5

 

빠른 개발 마무리를 위해서 대부분의 기능을 모두 구현 했습니다. 그래서, 이 중 내용 중 중요한 부분을 자세하게 설명하도록 하겠습니다.

 

남은 작업은 데이터들을 엑셀로 저장하는 부분만 남는데.. 이 포스트 작성 완료 후 그부분도 빠르게 작업 하도록 하겠습니다.

 

 

1. Windows Template Studio 1.3.17255.1 업데이트

 

전에 디자인 타임 화면이 깨지는 현상이 있었는데, 업데이트 이후 아직까지는 발생하지 않았습니다. 정확한 업데이트 노트를 보면 확실 하겠지만.. 그 업데이트 내용들 모두를 꼭 알필요는 없으니.. 일단 버전이 올라가면 업데이트를 하고 작업을 합니다.

 

 

업데이트 방법은

Tool -> Extensions and Updates -> Installed -> Windows Template Studio를 선택 후 업데이트를 진행 하거나, Updates를 선택 후 찾으셔서 업데이트를 진행 할 수 있습니다.

 

 

2. 앱바 사용하기

 

위에서 언급했던, 엑셀 저장 커맨드 버튼 앱에 추가 해야하는데, UWP의 경우 버튼들을 대부분 커맨드바에 배치를 하고 있습니다. 앱바 종류, 추가 방법 등을 알아 보도록 하겠습니다.

 

GridPage.xaml을 열고 -> Page를 선택하고 -> Properties -> Common 으로 찾아가면 BottomAppBar와 TopAppBar가 존재 합니다.

 

BottomAppBar는 화면 하단에 커맨드 바를 생성하고, TopAppBar는 화면 상단에 커맨드 바를 생성합니다.

Edge브라우저가 상단 커맨드를 잘 활용하고 있는 예입니다. 그리고, 요즘 트랜드가 상단 앱바를 이용하는 추세라.. 일단 상단을 만들어 보겠습니다.

 

 

 

사실 Bottom, Top은 단순히 앱바의 위치를 구분하는 것이고 진짜 필요한 것은 CommnadBar 컨트롤 입니다.

TopAppBar 오른쪽에 New 버튼을 클릭 -> Select Object 팝업 창에서 CommandBar를 선택 -> OK

 

AppBar와 CommandBar는 거의 비슷한 기능을 합니다. 차이점은 직접 찾아보시면 좋겠네요.

 

 

작업이 완료 되면 <Page.TopAppBar><CommandBar><Page.TopAppBar>라는 XAML 문이 추가가 되고, 오른쪽 Properties창에 TopAppBar가 확장되어서 CommandBar에 접근을 할 수 있는 상태가 됩니다.

여기서 PrimaryCommands 프로퍼티를 찾습니다.

 

 

<CommandBar>를 선택 -> Properties창 검색에 command를 입력합니다. -> Common -> PrimaryCommands, SecondaryCommands 두개의 프로퍼티를 찾을 수 있습니다.

 

PrimaryCommands는 직접 노출이 되는 버튼의 목록을 가지고 있습니다.

SecondaryCommands는 ... 버튼을 눌렀을 때 컨텍스트 메뉴 형태로 출력되는 명령 목록을 가지고 있습니다.

 

 

 

PrimaryCommands 프로퍼티 오른쪽의 ... 버튼을 클릭합니다. -> 팝업이 출력되며, 여기서 버튼들을 추가, 삭제, 수정 할 수 있습니다.

 

넣을 수 있는 오브젝트는 3가지 입니다.

AppBarButton : 기본 버튼 입니다.

AppBarSeparator : 버튼과 버튼 사이에 구분선을 넣습니다.

AppBarToggleButton : 토글 버튼 입니다.

 

기본적인 AppBarButton을 선택 하고, Add 버튼을 클릭하면 Items에 [0] AppBarButton이 추가된 것을 볼 수 있습니다. 추가된 녀석을 선택 하고, Properties에서 Icon을 찾은 후 별 모양을 선택하면 Symbol을 선택할 수 있게되며, 거기서 Save를 찾아서 선택합니다. 이렇게하면, 앱바에 디스크 모양의 아이콘을 가지고 있는 버튼이 추가되는 것입니다.

 

나머지는 Label이라는 프로퍼티를 찾아서 Save라는 택스트를 입력하고, 추후 Command 프로퍼티를 바인딩 하면 기본적인 작업이 완료됩니다. 현재는 버튼을 눌렀을 때 어떤 Command를 실행할지 정하지 못했으니 바인딩은 넘어 갑니다.

 

OK를 눌러 완료 합니다.

 

 

완료된 XAML 소스 입니다.

 

    <Page.TopAppBar>
        <CommandBar>
            <AppBarButton Icon="Save" Label="Save"/>
        </CommandBar>
    </Page.TopAppBar>

 

실행을 하면 아래와 같이 나오며, ... 버튼을 클릭해서 save라는 글씨를 확인 할 수 있습니다. 그런데 말입니다...뭔가 허전해진 것 같지 않은가요??

 

이렇게 상단 앱바를 만들어 넣으니..탭을 선택할 수 없게 되어 버렸습니다.;;;

그래서 결국 탭 컨트롤을 사용하는 경우에는 상단 앱바는 사용하지 못할 듯 합니다. BottomAppBar로 변경을 하도록 하죠..

 

xaml에서 TopAppBar를 BottomAppBar로 변경해 줍니다.

 

* 참! GridView가 전과는 좀 마니 달라졌습니다. 달라진 부분은 추후 간단하게 설명하겠습니다.

 

 

다시 실행을 하니 앱바가 하단에 출력됩니다. 음음..정상적으로 보이지만, 뭔가 이상한 점을 발견할 수 있을까요??

 

한번에 찾으셨다면 천재!!

 

전 분명히 GridPage에 추가를 했었는데, 탭을 Settings로 변경을 했는데도 여전히 앱바가 보이고 있습니다.

 

만약 탭이 아니라 Page단위로 navigate를 했다면, 보이지 않았겠지만.. 탭에서는 어쩔 수가...

 

이 문제를 해결하기 위해서 BottomAppBar를 PivotPage로 이동을 시키고, 탭이 변경이 될 때마다 앱바를 보이기/숨기기를 해야할 것 같습니다.

 

 

 

 

3. 앱바를 PivotPage에서 사용하기

 

GridPage에 추가했던 xaml소스를 복사해서 PivotPage 상단에 붙여 넣기를 합니다.

 

 

PivotViewModel.cs

 

프로퍼티를 추가합니다. GridPage가 보일 때는 앱바를 보이고, SettingsPage를 볼 때는 앱바를 숨기기 위한 기본 프로퍼티입니다.

 

        /// <summary>
        /// 앱바 보기
        /// </summary>
        public bool ShowAppBar
        {
            get => _showAppBar;
            set => Set(ref _showAppBar, value);
        }
        private bool _showAppBar;

 

기본 생성자에 PropertyChanged 이벤트 핸들러를 추가 합니다.

 

PivotViewModel은 Observable를 상속 받고 있기 때문에 자체적으로 PropertyChanged 이벤트를 발생 시킵니다. 그래서, SelectedView가 변경되면, 이벤트가 발생되고, 그 이벤트의 처리를 이벤트 핸들러가 진행 하게 됩니다.

 

SelectedView는 PivotItem이며, Content 프로퍼티에 들어가 있는 내용이 GridPage라면 ShowAppBar 프로퍼티는 true 값이 될 것입니다.

 

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

        }

 

빌드를 한 후 PivotPage.xaml로 이동합니다.

 

CommandBar에 마우스 커서를 올리거나 선택합니다. Properties창에서 Visibility 프로퍼티를 찾아 오른쪽 네모를 클릭 -> Create Data Binding... 메뉴 선택 -> 팝업 창에서 ShowAppBar 프로퍼티를 선택합니다.

 

자 여기까지 작업하고 생각을 해보겠습니다. ShowAppBar는 bool형 프로퍼티입니다. 그런데, 처음에 우리가 찾은 Visibility 프로퍼티는 Visibility타입으로 서로 다른 형태를 가지고 있습니다.

 

이렇게 서로 다른 데이터 형에 바인딩을 하는 경우에 중간에 데이터를 변경하거나, 형태를 변경하는 작업을 하는 녀석이 있는데, 이 녀석을 Converter라고 이야기 합니다.

 

팝업 왼쪽 하단을 보면 Converter를 선택할 수 있는 곳이 보이는데, 여기서 Add value converter..를 선택합니다.

 

 

 

Add Value Converter 팝업에서 Telerik.Core에 BoolToVisibilityConverter가 있는 것을 볼 수 있습니다. 이 컨버터의 역할은 true값이 들어오면 Visible, false값이 들어오면 Collapsed를 반환해 줍니다.

 

BoolToVisibilityConverter를 선택 후 OK를 누릅니다.

 

 

그렇게하면 xaml 소스에 컨버터가 자동으로 추가되며, 콤보 박스 목록에도 표시 됩니다.

 

BoolToVisibilityConverter를 선택하고 OK를 클릭해서 완료 합니다.

 

 

최종 xaml 소스

 

    <Page.Resources>
        <Core:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
    </Page.Resources>
    <Page.BottomAppBar>
        <CommandBar Visibility="{Binding ShowAppBar, Converter={StaticResource BoolToVisibilityConverter}}">
            <AppBarButton Icon="Save" Label="Save Excel" />
        </CommandBar>
    </Page.BottomAppBar>

 

이제 실행을 해보면 앱바가 탭이 변경될 때마다 보이기였다, 숨었다 합니다.

 

 

4. 내급여 앱의 정체!

 

이 앱은 기본 근무 시간 이 후의 시간에 대해서 수당을 계산해서 실제 지급되는 금액과 비교를 하기 위해서 만들어진 앱입니다.

 

 

근무 시간 입력 -> 일별 근무 시간을 이용해서 각 구간별 근무한 Minute을 구함 -> 월 총합 Minute를 구해서 시간.분으로 환산 -> 총합에 나머지 분은 절사를 하고 -> 시간에 대해서만 설정에서 입력한 금액을 곱해서 총 수당 금액을 구합니다.

 

그래서, 근무 시간을 이용해서 각 구간별로 근무 Minute를 구하는 부분을 어떻게 만들어야 하는지 고민을 좀 했습니다. 여러가지 고민을 하다보니, 모델에도 변경 사항이 발생했고, 화면도 좀 변하게 되었네요.. 마지막에 소스를 다시 올리도록 하겠습니다.

 

 

5. SettingsViewModel에서 입력한 기초 데이터를 GridViewModel에서 불러와서 사용하기..

 

기초 데이터 입/출력 부분을 만들 때는 한땀 한땀 넣었다, 뺐다를 했었는데요.. 다시 생각해보니 그렇게 작업할 이유가 없더군요..

 

SettingsViewModel 생성자에서 PayInformations에 기본 데이터를 밀어 넣도록 하고..

 

아래 코드 처럼 PayInformations 프로퍼티를 통째로 넣었다 뺐다 하면 끝입니다.

 

SettingsViewModel.cs

 

        private async void OnUnloaded()
        {
            //종료되기 전에 저장
            await ApplicationData.Current.LocalSettings.SaveAsync("PayInformations", PayInformations);
        }

        private async void OnLoaded()
        {
            //저장된 데이터 불러오기
            var pays = await ApplicationData.Current.LocalSettings.ReadAsync<IList<PayInformation>>("PayInformations");
            if (pays == null) return;
            PayInformations = pays;

        }

 

GridViewModel에서는 OnLoaded 시 동일하게 불러오기를 해서 null이 반환되면 메시지를 출력하도록 작업 했습니다.

 

await Task.Delay(500);

저장된 데이터를 불러오기 전에 0.5초 딜레이를 추가했습니다. 이유는 SettingsPage에서 내용을 수정하고, 저장을 완료하고 데이터를 다시 읽어와야 수정된 값을 가지고 올 수 있는데, 거의 동시에 이벤트가 발생하기 때문에 약간 딜레이를 줘서 문제를 해결하기 위해서 입니다.

 

async - await 패턴에는 이렇게 시간을 약간 딜레이를 줘서 작업을 해야하는 경우들도 자주 발생하니 참고하시면 될 것 같습니다.

 

StaticCommonHelper.ShowMessageBoxAsync는 간단하게 하나 만들어서 사용합니다. 소스는 바로 아래에 추가했습니다.

 

GridViewModel.cs

 

        private async void OnLoaded()
        {
            //Setting에서 저장되는 시간이 필요해서 .5초 후에 작업 시작
            await Task.Delay(500);

            //저장된 데이터 불러오기
            var pays = await ApplicationData.Current.LocalSettings.ReadAsync<IList<PayInformation>>("PayInformations");
            if (pays == null)
            {
                await StaticCommonHelper.ShowMessageBoxAsync("Please create basic data in setting screen");
                return;
            }
            PayInformations = pays;

        }

 

 

MessageDialog를 이용하면 간단한 메시지를 출력할 수 있습니다.

 

StaticCommonHelper.cs

 

namespace MyPay.Helpers
{
    public static class StaticCommonHelper
    {
        public static async Task ShowMessageBoxAsync(string message)
        {
            var messageBox = new MessageDialog(message);
            await messageBox.ShowAsync();
        }
    }
}

 

 

 

GridViewModel에서 PayInformations가 필요한 이유는 계산을 위해서 입니다.

 

계산을 하기 위해서는 이벤트가 발생해야 하고 해당 이벤트가 발생했을 때 계산 작업을 하면 됩니다.

 

어떤 이벤트를 사용할지 찾아보니 RadDataGrid에서는 SelectionChanged 이벤트가 자주 발생하는 것을 볼 수 있었습니다. 데이터가 수정된 후에 발생하는 이벤트가 있을까 찾아보았지만.. 딱히 눈에 보이는 녀석이 없더군요.. 물론 일반적으로 목록형 컨트롤에서 이 이벤트를 잘 사용하기는 하지만, 여기서까지 사용할 줄은..

 

 

RadDataGrid와 같이 여러개의 이벤트를 가지고 있는 컨트롤들은 각각의 이벤트들을 커맨드와 연결 시킬 수 있는데, 이 때 사용하는 녀석이 EventTriggerBehavior와 InvokeCommandAction 입니다.

 

블랜드를 이용하면 드래그&드롭으로 컨트롤과 쉽게 연결을 시킬 수 있는데, 이미지로 설명을 하기가 쉽지 않으니..그냥 XAML 소스로 설명을 하도록 하겠습니다.

 

일단 EventTriggerBehavior와 InvokeCommandAction등은 Behavior라고 이야기 하며,

xaml 에서는 아래와 같이 사용할 수 있습니다. GridPage 상단에 아래 코드를 추가 합니다.

 

GridPage.xaml

 

    xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
    xmlns:Core="using:Microsoft.Xaml.Interactions.Core"

 

 

RadDataGrid 컨트롤 내부에 아래와 같은 코드를 추가해 줍니다.

SelectionChanged이벤트를 SelectionChangedCommand와 연결해 줍니다. 그러면, 컨트롤에서 해당 이벤트가 발생하면 뷰모델에 있는 SelectionChangedCommand를 실행 시켜 줍니다.

 

        <tg:RadDataGrid Grid.Row="1" ColumnDataOperationsMode="Flyout"
                        ItemsSource="{Binding Works}" AutoGenerateColumns="False" UserEditMode="Inline"
                        UserGroupMode="Disabled">
            <tg:RadDataGrid.Columns>
                <tg:DataGridDateColumn PropertyName="WorkDay" Header="Work Day" CellContentFormat="{}{0:d}"
                                       SizeMode="Fixed" />
                <tg:DataGridBooleanColumn Header="Holiday" PropertyName="IsHoliday" SizeMode="Fixed" Width="80" />
                <tg:DataGridTimeColumn PropertyName="StartWork" Header="Start" CellContentFormat="{}{0:t}"
                                       SizeMode="Fixed" />
                <tg:DataGridTimeColumn PropertyName="EndWork" Header="End" CellContentFormat="{}{0:t}"
                                       SizeMode="Fixed" />
                <tg:DataGridTextColumn PropertyName="BasicWorkTime" Header="1.0 min"
                                       CanUserEdit="False" />
                <tg:DataGridTextColumn PropertyName="OverTime15" Header="1.5 min"
                                       CanUserEdit="False" />
                <tg:DataGridTextColumn PropertyName="OverTime20" Header="2.0 min"
                                       CanUserEdit="False" />
                <tg:DataGridTextColumn PropertyName="OverTime25" Header="2.5 min"
                                       CanUserEdit="False" />
                <tg:DataGridTextColumn PropertyName="TodayWorkTime" Header="Total min"
                                       CanUserEdit="False" />
            </tg:RadDataGrid.Columns>
            <Interactivity:Interaction.Behaviors>
                <Core:EventTriggerBehavior EventName="SelectionChanged">
                    <Core:InvokeCommandAction Command="{Binding SelectionChangedCommand}" />
                </Core:EventTriggerBehavior>
            </Interactivity:Interaction.Behaviors>
        </tg:RadDataGrid>

 

 

 

GridViewModel.cs

 

프로퍼티를 하나 추가하고

 

        /// <summary>
        /// SelectionChangedCommand
        /// </summary>
        public ICommand SelectionChangedCommand { get; set; }

 

Init() 에 아래 한 줄을 추가해 줍니다.

 

            SelectionChangedCommand = new RelayCommand(ExecuteSelectionChangedCommand);

 

ExecuteSelectionChangedCommand()를 만들어 줍니다.

 

        private void ExecuteSelectionChangedCommand()
        {
        }

 

{ 이 부분에 F9를 눌러 브레이크 포인트를 걸고 실행해서 RadDataGrid에서 선택을 변경할 때마다 ExecuteSelectionChangedCommand가 호출이 되는지 확인 합니다.

 

여기 까지 완료가 되었다면, 이제 본격적으로 계산을 위한 코드를 입력합니다.

 

GetWorkMinute()를 추가합니다.

시작시간, 종료시간, 휴일유무, 가중치를 넣어주면, 가중치에 해당하는 시간 구간에 근무한 Minute이 반환됩니다.

 

이 때 사용하기 위해서 PayInformations를 미리 불러온 것이지요.

 

이런 로직을 만들 때 이해하기 쉽고, 빠르기 때문에 LINQ식을 즐겨 사용합니다. 이런 로직을 하나하나 만들었다면, 아마 코드가 무척이나 길어지고, 복잡한 코드가 되었을 것이라고 생각합니다.

 

그리고 VS2015까지는 LINQ식 중간에 브레이크 포이트를 걸지 못했지만, VS2017은 중간에 브레이크 포이트를 걸어서 데이터 확인이 가능합니다. 다만, 어느정도 한계는 존재하니 잘 활용하면 좋을 것 같습니다.

 

        /// <summary>
        ///     분 반환
        /// </summary>
        /// <param name="workStart"></param>
        /// <param name="workEnd"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        private double GetWorkMinute(DateTime workStart, DateTime workEnd, bool isHoliday, double value)
        {
            var mins = from item in PayInformations
                //차이가 0.1이하인 녀석들만
                where Math.Abs(item.Value - value) < 0.1
                      //휴일 유무가 동일한 녀석만
                      && item.IsHoliday == isHoliday
                //시작시간이 종료시간보다 크면 종료시간을 다음 일자로
                let workEnd2 = workStart > workEnd ? workEnd.AddDays(1) : workEnd
                //비교 시작 시간을 근무일 기준 시간으로 변경
                let itemStart = DateTime.Parse($"{workStart:yyyy-MM-dd} {item.StartTime:HH:mm}")
                //비교 종료 시간을 근무일 기준 종료시간으로 임시 변경
                let itemEndTemp = DateTime.Parse($"{workStart:yyyy-MM-dd} {item.EndTime:HH:mm}")
                //비교 시작 시간이 비교 종료 시간보다 크면 비교 종료 시간을 다음날로 변경
                let itemEnd = itemStart > itemEndTemp ? itemEndTemp.AddDays(1) : itemEndTemp
                //계산 범위 밖의 데이터는 먼저 거름
                let ts = workEnd2 < itemStart || workStart > itemEnd
                    ? TimeSpan.Zero
                    //근무 시작 시간이, 비교 시작 시간보다 작거나 같고, 근무 종료시간도, 비교 종료 시간보다 크거나 같으면
                    : workStart <= itemStart && workEnd2 >= itemEnd
                        //비교 종료 시간에서 비교 시작 시간을 뺀다
                        ? itemEnd.Subtract(itemStart)
                        //근무 시작 시간이, 비교 시작 시간보다 크고, 근무 종료 시간이, 비교 종료 시간보다 크거나 같으면
                        : workStart > itemStart && workEnd2 >= itemEnd
                            //비교 종료 시간에서 근무 시작 시간을 뺀다
                            ? itemEnd.Subtract(workStart)
                            //근무 시작 시간이, 비교 시작 시간보다 작거나 같고, 근무 종료시간이, 비교 종료 시간보다 작으면
                            : workStart <= itemStart && workEnd2 < itemEnd
                                //근무 종료시간에서 비교 시작 시간을 뺀다
                                ? workEnd2.Subtract(itemStart)
                                //아무것도 아니면 0
                                : TimeSpan.Zero
                select ts.TotalMinutes;
            return mins.Sum(p => p);
        }

 

위 함수의 테스트를 위해서 테스트 코드를 입력해서 확인을 하는 것도 좋습니다.

 

 

ExecuteSelectionChangedCommand() 메소드에 아래 코드를 추가합니다.

 

Works는 지난달 1일부터 말일까지의 데이터가 들어가 있으며, 각 시간 구간별로 근무 Minute 구해서 입력합니다.

 

LINQ와 Lambda Expressions을 이용해서 계산을 하게되면, foreach를 이용한 방법보다 간결하고 빠르게 처리가 가능합니다. 아래 식에서 .Count()를 사용한 이유는 LINQ를 실행하기 위한 코드 입니다.

 

            var set = (from item in Works
                //시작시간과 종료시간이 서로 다른 경우에만
                where item.StartWork != item.EndWork
                let min10 = item.BasicWorkTime = GetWorkMinute(item.StartWork, item.EndWork, item.IsHoliday, 1.0)
                let min15 = item.OverTime15 = GetWorkMinute(item.StartWork, item.EndWork, item.IsHoliday, 1.5)
                let min20 = item.OverTime20 = GetWorkMinute(item.StartWork, item.EndWork, item.IsHoliday, 2.0)
                let min25 = item.OverTime25 = GetWorkMinute(item.StartWork, item.EndWork, item.IsHoliday, 2.5)
                let total = item.TodayWorkTime = min10 + min15 + min20 + min25
                select item).Count();

 

월 합계, 절사 데이터, 수당 총액은 모두 TotalWorks에 넣도록 되어 있습니다.

 

        /// <summary>
        ///     합계
        /// </summary>
        public IList<WorkItem> TotalWorks
        {
            get => _totalWorks;
            set => Set(ref _totalWorks, value);
        }
        private IList<WorkItem> _totalWorks;

 

 

            //월 합계
            var monthTotal = TotalWorks.First(p => p.Id == 100);
            monthTotal.BasicWorkTime = GetHourMin(Works.Sum(p => p.BasicWorkTime));
            monthTotal.OverTime15 = GetHourMin(Works.Sum(p => p.OverTime15));
            monthTotal.OverTime20 = GetHourMin(Works.Sum(p => p.OverTime20));
            monthTotal.OverTime25 = GetHourMin(Works.Sum(p => p.OverTime25));
            monthTotal.TodayWorkTime = GetHourMin(Works.Sum(p => p.TodayWorkTime));

            //절사 데이터
            var truncationTotal = TotalWorks.First(p => p.Id == 101);
            truncationTotal.BasicWorkTime = GetHourMin(Works.Sum(p => p.BasicWorkTime), true);
            truncationTotal.OverTime15 = GetHourMin(Works.Sum(p => p.OverTime15), true);
            truncationTotal.OverTime20 = GetHourMin(Works.Sum(p => p.OverTime20), true);
            truncationTotal.OverTime25 = GetHourMin(Works.Sum(p => p.OverTime25), true);
            truncationTotal.TodayWorkTime = GetHourMin(Works.Sum(p => p.TodayWorkTime), true);

 

GetHourMin() 추가

Minute를 시간.분 으로 반환합니다.

 

        /// <summary>
        ///     시간.분 형태로 만들어서 반환
        /// </summary>
        /// <returns></returns>
        private double GetHourMin(double source, bool truncation = false)
        {
            var hour = Convert.ToInt32(source) / 60;
            var min = truncation == false ? source % 60 : 0;
            return double.Parse($"{hour}.{min}");
        }

 

ExecuteSelectionChangedCommand()에 마지막 코드를 추가합니다.

 

이 LINQ에서는 Sub query를 사용하고 있으며, 이런 sub query도 사용이 가능하다는 것을 알고 넘어 가시면 될 것 같습니다.

 

            //수당
            var pay = (from payTotal in TotalWorks
                where payTotal.Id == 102
                let subSet = (from item in PayInformations
                let pay15 = Math.Abs(item.Value - 1.5) < 0.1 ? item.TimePay : 0
                let pay20 = Math.Abs(item.Value - 2.0) < 0.1 ? item.TimePay : 0
                let pay25 = Math.Abs(item.Value - 2.5) < 0.1 ? item.TimePay : 0
                select new {Pay15 = pay15, Pay20 = pay20, Pay25 = pay25})

                let maxSubSet = new
                {
                    MaxPay15 = subSet.Max(p => p.Pay15),
                    MaxPay20 = subSet.Max(p => p.Pay20),
                    MaxPay25 = subSet.Max(p => p.Pay25)
                }
                let setPay15 = payTotal.OverTime15 = truncationTotal.OverTime15 * maxSubSet.MaxPay15
                let setPay20 = payTotal.OverTime20 = truncationTotal.OverTime20 * maxSubSet.MaxPay20
                let setPay25 = payTotal.OverTime25 = truncationTotal.OverTime25 * maxSubSet.MaxPay25
                let setTotal = payTotal.TodayWorkTime = setPay15 + setPay20 + setPay25
                select payTotal).Count();

 

 

마지막으로, 화면에 출력할 때 그리드를 3개로 나누어서 출력하고 있습니다. 2개로 어떻게 해볼려고 했는데..시간과 돈이라는 서로 단위가 다르다 보니 동일한 컬럼에 보이면 의미가 좀 달라져서.. TotalWorks라는 데이터 목록은 그대로 두고, 바인딩되는 프로퍼티를 2개를 더 만들어서 사용했습니다.

 

TotalWorks에 반드시 3개의 데이터가 존재해야 합니다. Lambda Expressions을 사용하는 예로 보아주시면 좋겠습니다. 물론 가장 좋은 선택은 2개의 완전 분리된 프로퍼티를 사용하는 것입니다.

 

        /// <summary>
        /// 합계 2줄만..
        /// </summary>
        public IList<WorkItem> TotalWorks2Row => TotalWorks?.Take(2).ToList();
        /// <summary>
        /// 금액줄..
        /// </summary>
        public IList<WorkItem> TotalWorksPay => TotalWorks?.Skip(2).Take(1).ToList();

 

 

GridPage.xaml

 

        <tg:RadDataGrid Grid.Row="2" ColumnDataOperationsMode="Flyout" Margin="0 10 0 0"
                        ItemsSource="{Binding TotalWorks2Row}" AutoGenerateColumns="False"
                        UserGroupMode="Disabled" FrozenColumnCount="1">
            <tg:RadDataGrid.Columns>
                <tg:DataGridTextColumn PropertyName="Description" Header="Gubun" SizeMode="Fixed" />
                <tg:DataGridTextColumn PropertyName="BasicWorkTime" Header="1.0 H.m" CellContentFormat="{}{0:f2}" />
                <tg:DataGridTextColumn PropertyName="OverTime15" Header="1.5 H.m" CellContentFormat="{}{0:f2}" />
                <tg:DataGridTextColumn PropertyName="OverTime20" Header="2.0 H.m" CellContentFormat="{}{0:f2}" />
                <tg:DataGridTextColumn PropertyName="OverTime25" Header="2.5 H.m" CellContentFormat="{}{0:f2}" />
                <tg:DataGridTextColumn PropertyName="TodayWorkTime" Header="Total H.m" CellContentFormat="{}{0:f2}" />
            </tg:RadDataGrid.Columns>
        </tg:RadDataGrid>

 

        <tg:RadDataGrid Grid.Row="3" ColumnDataOperationsMode="Flyout" Margin="0 10 0 0"
                        ItemsSource="{Binding TotalWorksPay}" AutoGenerateColumns="False"
                        UserGroupMode="Disabled" FrozenColumnCount="1">

            <tg:RadDataGrid.Columns>
                <tg:DataGridTextColumn PropertyName="Description" Header="Gubun" SizeMode="Fixed" />
                <tg:DataGridTextColumn PropertyName="BasicWorkTime" Header="1.0" CellContentFormat="{}{0:c0}" />
                <tg:DataGridTextColumn PropertyName="OverTime15" Header="1.5 $" CellContentFormat="{}{0:c0}" />
                <tg:DataGridTextColumn PropertyName="OverTime20" Header="2.0 $" CellContentFormat="{}{0:c0}" />
                <tg:DataGridTextColumn PropertyName="OverTime25" Header="2.5 $" CellContentFormat="{}{0:c0}" />
                <tg:DataGridTextColumn PropertyName="TodayWorkTime" Header="Total $" CellContentFormat="{}{0:c0}" />
            </tg:RadDataGrid.Columns>
        </tg:RadDataGrid>

 

 

6. 실행화면

 

엇 화면 보니 버그가 있군요..버그 픽스는 다음에..

 

 

 

7. 소스

 

MyPay_part5.zip

 

 

 

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

.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
내급여 UWP 앱 개발 part2  (4) 2017.08.24
블로그 이미지

kaki104

/// Microsoft MVP - Windows Development - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

Tag UWP

내급여 UWP 앱 개발

Part1

Part2

Part3

Part4

 

 

**. 설정의 급여 기본 정보

 

급여 정보 모델을 수정 했습니다.

 

시작시간, 종료시간, 시급 프로퍼티는 기본 값으로 입력된 후 저장된 데이터를 불러와서 변경을 시켜주게 되는데, 변경된 데이터가 화면에 갱신이 되도록 하려면 아래와 같이 수정해 주어야 합니다.

 

값과 휴일 유무는 개발을 진행하다가 추가된 프로퍼티들입니다.  

 

1. PayInformation

 


    ///     급여 정보
    /// </summary>
    public class PayInformation : Observable
    {
        private DateTime _endTime;
        private DateTime _startTime;
        private int _timePay;


        /// <summary>
        ///     아이디
        /// </summary>
        public string Id { get; set; }

        /// <summary>
        ///     값
        /// </summary>
        public double Value { get; set; }

        /// <summary>
        ///     휴일 유무
        /// </summary>
        public bool IsHoliday { get; set; }

        /// <summary>
        ///     시작시간
        /// </summary>
        public DateTime StartTime
        {
            get => _startTime;
            set => Set(ref _startTime, value);
        }

        /// <summary>
        ///     종료시간
        /// </summary>
        public DateTime EndTime
        {
            get => _endTime;
            set => Set(ref _endTime, value);
        }

        /// <summary>
        ///     시급
        /// </summary>
        public int TimePay
        {
            get => _timePay;
            set => Set(ref _timePay, value);
        }

    }

 

 

2. SettingViewModel

 

설정 뷰 모델 생성자 수정 했습니다.

생각을 해보니 평일과 휴일를 각각 입력해야 하더군요. 그래서 생성자에서 각 단계별로 4개씩 총 8개를 넣어 놓습니다.

 

        /// <summary>
        ///     기본 생성자
        /// </summary>
        public SettingsViewModel()
        {
            SwitchThemeCommand = new RelayCommand(async () => { await ThemeSelectorService.SwitchThemeAsync(); });

            LoadedCommand = new RelayCommand(OnLoaded);
            UnloadedCommand = new RelayCommand(OnUnloaded);

            PayInformations = new List<PayInformation>
            {
                new PayInformation
                {
                    Id = "1.0",
                    Value = 1.0,
                    StartTime = DateTime.Parse("08:00"),
                    EndTime = DateTime.Parse("18:00"),
                    TimePay = 10000
                },
                new PayInformation
                {
                    Id = "1.5 Part1",
                    Value = 1.5,
                    StartTime = DateTime.Parse("08:00"),
                    EndTime = DateTime.Parse("18:00"),
                    TimePay = 10000
                },
                new PayInformation
                {
                    Id = "2.0",
                    Value = 2.0,
                    StartTime = DateTime.Parse("08:00"),
                    EndTime = DateTime.Parse("18:00"),
                    TimePay = 10000
                },
                new PayInformation
                {
                    Id = "1.5 Part2",
                    Value = 1.5,
                    StartTime = DateTime.Parse("08:00"),
                    EndTime = DateTime.Parse("18:00"),
                    TimePay = 10000
                },
                new PayInformation
                {
                    Id = "1.5 Holiday",
                    Value = 1.5,
                    IsHoliday = true,
                    StartTime = DateTime.Parse("08:00"),
                    EndTime = DateTime.Parse("18:00"),
                    TimePay = 10000
                },
                new PayInformation
                {
                    Id = "2.0 Holiday1",
                    Value = 2.0,
                    IsHoliday = true,
                    StartTime = DateTime.Parse("08:00"),
                    EndTime = DateTime.Parse("18:00"),
                    TimePay = 10000
                },
                new PayInformation
                {
                    Id = "2.5 Holiday",
                    Value = 2.5,
                    IsHoliday = true,
                    StartTime = DateTime.Parse("08:00"),
                    EndTime = DateTime.Parse("18:00"),
                    TimePay = 10000
                },
                new PayInformation
                {
                    Id = "2.0 Holiday2",
                    Value = 2.0,
                    IsHoliday = true,
                    StartTime = DateTime.Parse("08:00"),
                    EndTime = DateTime.Parse("18:00"),
                    TimePay = 10000
                }

            };
        }

 

 

3. 저장하기와 불러오기

 

데이터 저장하기와 불러오기는 ApplicationData Class 를 이용합니다.

 

저장을 먼저 해보죠..

ApplicationData.Current.LocalSettings는 앱 데이터를 저장할 수 있는 고유 공간입니다. 그리고 뒤에 SaveAsync는 확장 메소드로 F12(정의로 이동)를 눌러보시면 어떻게 동작하는지 아실 수 있을 것입니다.

 

OnLoaded가 실행될 때 저장되어있던 데이터를 ReadAsync라는 확장 메소드를 이용해 불러와서 시작시간 종료시간 시급을 넣어 줍니다. 물론 저장된 내용이 없으면, 그냥 기존데이터를 보여주겠죠?

 

        private async void OnUnloaded()
        {
            //종료되기 전에 저장
            foreach (var pay in PayInformations)
                await ApplicationData.Current.LocalSettings.SaveAsync(pay.Id, pay);
        }

        private async void OnLoaded()
        {
            //저장된 데이터 불러오기
            foreach (var pay in PayInformations)
            {
                var savedPay = await ApplicationData.Current.LocalSettings.ReadAsync<PayInformation>(pay.Id);
                if (savedPay == null) continue;
                pay.StartTime = savedPay.StartTime;
                pay.EndTime = savedPay.EndTime;
                pay.TimePay = savedPay.TimePay;
            }
        }

 

 

4. SettingPage.xam

 

그리드에 데이터가 8줄이 들어가기 때문에 강제로 컨트롤의 높이를 200으로 한정했으며, RadDataGrid를 감싸고 있던  StackPanel을 제거 했습니다. 또한, 새로 추가한 프로퍼티를 보여줄 컬럼을 추가합니다.

 

            <grid:RadDataGrid Grid.Row="1" Margin="0,16,0,0" ColumnDataOperationsMode="Flyout" UserEditMode="Inline"
                              UserGroupMode="Disabled" AutoGenerateColumns="False"
                              ItemsSource="{Binding PayInformations}" HorizontalAlignment="Left"
                              Height="200">
                <grid:RadDataGrid.Columns>
                    <grid:DataGridTextColumn Header="Id" PropertyName="Id" CanUserEdit="False" SizeMode="Auto"/>
                    <grid:DataGridBooleanColumn Header="Holiday" PropertyName="IsHoliday" SizeMode="Fixed" Width="80" />
                    <grid:DataGridTimeColumn PropertyName="StartTime" Header="Start" CellContentFormat="{}{0:t}"
                                             SizeMode="Auto" />
                    <grid:DataGridTimeColumn PropertyName="EndTime" Header="End" CellContentFormat="{}{0:t}"
                                             SizeMode="Auto" />
                    <grid:DataGridNumericalColumn Header="Hour Pay" PropertyName="TimePay" SizeMode="Auto" CellContentFormat="{}{0:n0}"/>

                </grid:RadDataGrid.Columns>
            </grid:RadDataGrid>

 

 

 

이정도 수정을 한 후 실행 후 수정을 원하는 컬럼을 선택하면 아래와 같이 나옵니다.

 

 

저장된 데이터는 어디에 들어가 있을까요?

 

제 컴퓨터에서는 아래 경로에 settings.dat 파일이 존재합니다. 

 

C:\Users\MunChan Park\AppData\Local\Packages\CA3D44E9-05D5-4474-AB5D-89CB15FA4C9E_v0qv1p8057pc0\Settings

 

폴더명이 좀 어렵기 때문에 쉽게 찾을 수는 없습니다.

 

 

5. 기준월 추가

 

GridViewModel

 

앱이 시작하면 지난달을 기준 월로 지정하도록 만들었으며, 문자열 형태입니다.

 

        /// <summary>

        ///     기준 월
        /// </summary>
        public string BaseMonth
        {
            get => _baseMonth;
            set => Set(ref _baseMonth, value);
        }

        private string _baseMonth; 

 

지난달은 입력은 AddMonths를 이용하면 됩니다.

 

        private void Init()
        {
            LoadedCommand = new RelayCommand(OnLoaded);
            UnloadedCommand = new RelayCommand(OnUnloaded);

            BaseMonth = DateTime.Now.AddMonths(-1).ToString("yyyy-MM");

            PropertyChanged += GridViewModel_PropertyChanged;

            CreateMonthData();
        }

 

GridPage.xaml

 

기준 월을 출력하는 것은 화면 오른쪽 상단에 출력하며, 기준월을 변경할 경우를 대비해서 TwoWay를 이용합니다.

 

        <StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
            <TextBlock Text="Month" VerticalAlignment="Center"/>
            <TextBox Text="{Binding BaseMonth, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>

 

 

6. 1일부터 월말까지 데이터 자동 생성

 

기준월이 정해지면 해당 월의 1일부터 월말 까지 데이터를 자동으로 생성해서 화면에 출력합니다.

 

우선 1일부터 31일까지를 배열로 만들어 놓고, GetDate 함수를 이용해서 진짜 존재하는 일자인지 확인해서 반환하고 결과가 null이면 포함하지 않도록 하구요 무슨 요일인지도 반환하도록 해서 주말이면, IsHoliday를 설정하도록 합니다.

 

        private readonly int[] _monthData =
        {
            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
            30, 31
        };

       

        private void CreateMonthData()
        {
            if (string.IsNullOrEmpty(BaseMonth)) return;
            //31개의 데이터를 입력
            Works = (from num in _monthData
                let dayNullable = GetDate($"{BaseMonth}-{num}")
                where dayNullable != null
                let day = (DateTime) dayNullable
                let week = day.DayOfWeek
                let holiday = week == DayOfWeek.Saturday || week == DayOfWeek.Sunday
                select new WorkItem
                {
                    Id = num,
                    WorkDay = day,
                    IsHoliday = holiday,
                    StartWork = holiday
                        ? DateTime.Parse($"{day:yyyy-MM-dd} 00:00")
                        : DateTime.Parse($"{day:yyyy-MM-dd} 08:00"),
                    EndWork = holiday
                        ? DateTime.Parse($"{day:yyyy-MM-dd} 00:00")
                        : DateTime.Parse($"{day:yyyy-MM-dd} 18:00")
                }).ToList();
        }

 

        /// <summary>
        ///     문자열 날짜를 데이트타임 형으로 변환
        /// </summary>
        /// <param name="date"></param>
        /// <returns></returns>
        private DateTime? GetDate(string date)
        {
            DateTime returnDate;
            if (DateTime.TryParse(date, out returnDate))
                return returnDate;
            return null;
        }

 

 

 

 

7. 기타

 

작업을 하다보니, 모델도 수정하고, 그리드도 수정을 해서.. 오늘까지의 소스는 여기에 올리도록 하겠습니다.

 

MyPay_part4.zip

 

 

8. 주의

 

                    <tg:DataGridDateColumn PropertyName="WorkDay" Header="Work Day" CellContentFormat="{}{0:d}"
                                           SizeMode="Fixed" />

 

RadDataGrid의 첫번째 컬럼을 CanUserEdit="False"로 변경하면,

 

System.NullReferenceException: Object reference not set to an instance of an object.
   at Telerik.UI.Xaml.Controls.Grid.XamlGridEditCellGenerator.GenerateContainerForItem(CellGenerationContext info, Object containerType)
   at Telerik.UI.Xaml.Controls.Grid.CellEditorModelGenerator.GenerateContainerForItem(CellGenerationContext context, Object containerType)
   at Telerik.UI.Xaml.Controls.Grid.ItemModelGenerator`2.GenerateContainer(K context)
   at Telerik.UI.Xaml.Controls.Grid.CellsController`1.GetCell

 

수정하려고 했을 때 에러가 발생합니다. 사실 이 에러가 발생해서 맨붕이 와서..다른걸로 변경하려다가....흐흐흐

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

내급여 UWP 앱 개발 part6  (0) 2017.09.25
내급여 UWP 앱 개발 part5  (0) 2017.09.18
내급여 UWP 앱 개발 part4  (0) 2017.09.04
내급여 UWP 앱 개발 part3  (0) 2017.08.26
내급여 UWP 앱 개발 part2  (4) 2017.08.24
내급여 UWP 앱 개발 part1  (0) 2017.08.22
블로그 이미지

kaki104

/// Microsoft MVP - Windows Development - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

티스토리 툴바