블로그 이미지
* Microsoft MVP - Windows Development 2014 ~ 2020 http://youtube.com/FutureOfDotNet kaki104

카테고리

List All (601)
Visual Studio (6)
Blazor (2)
Windows App(Universa.. (100)
Xamarin Forms (4)
Bot Framework (19)
Azure (9)
Windows 10 (52)
WPF (7)
Facebook News & Tips (158)
Windows 8&8.1 (113)
Windows Phone 8 (42)
Silverlight (37)
HTML5 & MVC4 (16)
Portable Class Library (2)
Uncategorised Tips a.. (3)
Kinect for Windows (2)
ETC (12)
kaki104 Scrap (4)
App News (11)
Total543,763
Today11
Yesterday62

내급여 UWP 앱 개발

Part1

Part2

Part3

Part4

 

 

1. 나머지 컬럼 추가

 

어제 3개의 컬럼까지 추가해 보았습니다. 오늘은 나머지 컬럼을 모두 추가하도록 하겠습니다.

 

아래와 같이 나머지 컬럼도 추가하면서 아래 기능도 함께 추가합니다.

 

UserEditMode="Inline" : 줄 단위로 수정 할 수 있도록 합니다.

UserGroupMode="Disabled" : 쓸데 업는 그룹핑 기능은 막습니다.

FrozenColumnCount="1" : 첫번째 컬럼을 고정 컬럼으로 놓습니다.

 

SizeMode="Fixed" : 날짜나 시간을 수정하려면 컬럼이 대폭 넓어져서 가로로 스크롤이 생깁니다. 그래서 사이즈를 고정 시켜 버립니다.

 

            <tg:RadDataGrid ColumnDataOperationsMode="Flyout" x:Name="grid"
                            ItemsSource="{Binding Works}" AutoGenerateColumns="False" UserEditMode="Inline"
                            UserGroupMode="Disabled" FrozenColumnCount="1">
                <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:DataGridTimeColumn PropertyName="BasicWorkTime" Header="1.0" CellContentFormat="{}{0:HH:mm}"
                                           CanUserEdit="False" />
                    <tg:DataGridTimeColumn PropertyName="OverTime15" Header="1.5" CellContentFormat="{}{0:HH:mm}"
                                           CanUserEdit="False" />
                    <tg:DataGridTimeColumn PropertyName="OverTime20" Header="2.0" CellContentFormat="{}{0:HH:mm}"
                                           CanUserEdit="False" />
                    <tg:DataGridTimeColumn PropertyName="OverTime25" Header="2.5" CellContentFormat="{}{0:HH:mm}"
                                           CanUserEdit="False" />

                    <tg:DataGridTimeColumn PropertyName="TodayWorkTime" Header="Total" CellContentFormat="{}{0:HH:mm}"
                                           CanUserEdit="False" />
                    <tg:DataGridTextColumn Header="Description" PropertyName="Description" />
                </tg:RadDataGrid.Columns>
            </tg:RadDataGrid>

 

실행 상태입니다.

 

 

일자를 수정하기 위해 클릭하면 날짜 선택 팝업이 출력되면서 입력할 수 있습니다.

 

 

 

2. 급여 기본 정보 작업 시작

 

설정창에서 급여 기본 정보를 입력 받아서 처리를 해야하니..그 쪽 작업을 좀 하도록 하겠습니다.

 

먼저!

 

모델을 추가해 볼까요?

 

위치는 Models 폴더에 PayInformation이라는 모델을 추가합니다.

 

using System;

namespace MyPay.Models
{
    /// <summary>
    ///     급여 정보
    /// </summary>
    public class PayInformation
    {
        /// <summary>
        ///     아이디 1.0, 1.5, 2.0, 2.5
        /// </summary>
        public string Id { get; set; }

        /// <summary>
        ///     시작시간
        /// </summary>
        public DateTime StartTime { get; set; }

        /// <summary>
        ///     종료시간
        /// </summary>
        public DateTime EndTime { get; set; }

        /// <summary>
        ///     시급
        /// </summary>
        public int TimePay { get; set; }
    }
}

 

 

3. SettingsViewModel 뷰 모델에 디자인 타임 데이터 만들기

 

디자인 화면 작업을 위해서 디자인 타임 데이터를 추가해 놓습니다. 나중에는 저장되어 있던 내용을 불러와서 설정해 줘야 합니다.

 

        }
        {
            SwitchThemeCommand = new RelayCommand(async () => { await ThemeSelectorService.SwitchThemeAsync(); });

            PayInformations = new List<PayInformation>
            {
                new PayInformation{Id = "1.0", StartTime = DateTime.Parse("08:00"), EndTime = DateTime.Parse("18:00"), TimePay = 10000},
                new PayInformation{Id = "1.5", StartTime = DateTime.Parse("08:00"), EndTime = DateTime.Parse("18:00"), TimePay = 10000},
                new PayInformation{Id = "2.0", StartTime = DateTime.Parse("08:00"), EndTime = DateTime.Parse("18:00"), TimePay = 10000},
                new PayInformation{Id = "2.5", StartTime = DateTime.Parse("08:00"), EndTime = DateTime.Parse("18:00"), TimePay = 10000},
            };

        }

 

빌드를 하고 SettingPage.xaml.cs을 열어 줍니다.

 

 

4. SettingPage.xaml.cs

 

기존에 작성되어있던 ViewModel을 삭제하고 아래와 같이 변경해 줍니다. 기 작성되어 있던 녀석은 x:Bind를 사용하기 위해서 사용된 것인데...저는 x:Bind 보다는 일반 Binding 사용하는 것을 좋아 하기 때문입니다.

 

x:Bind와 Binding의 차이점은 구글에서 x:bind vs binding으로 검색하시면 여러가지 내용이 나오며 아래 링크를 참고 하셔도 될 것 같습니다.

https://stackoverflow.com/questions/37398038/difference-between-binding-and-xbind 

 

    }
    {
        public SettingsPage()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 뷰모델
        /// </summary>
        public SettingsViewModel ViewModel => DataContext as SettingsViewModel;

    }

 

 

5. SettingPage.xaml

 

SettingPage.xaml을 열어서 DataContext에 뷰모델을 연결하고, 기존에 바인딩되어 있던 녀석들을 수정해 줍니다.

 

Page를 선택하고, 프로퍼티창에서 DataContext를 찾고 New 버튼을 클릭합니다. Select Object 창에서 setting이라는 단어를 입력해서 필터링하고 SettingViewModel을 선택하고 OK를 눌러서 마무리 합니다.

 

SettingViewModel을 보이지 않는다면, 빌드를 하신 후 다시 시도하시면 됩니다.

 

 

기존 토글스위치 컨트롤에 InOn 프로퍼티에 연결된 바인딩을 수정해 줍니다.

 

ToggleSwitch를 선택하고 InOn 프로퍼티를 찾습니다. 오른쪽 노란색 네모를 클릭하면 컨텍스트 메뉴가 출력되고 Create Data Binding...을 선택합니다.

Create Data Binding for (ToggleSwitch).IsOn 이라는 팝업에서 IsLightThemeEnabled를 선택하고 OK를 클릭합니다.

 

팝업이 아래와 같이 나오지 않는다면, DataContext에 SettingViewModel이 연결이 되어 있지 않기 때문입니다.

 

 

InvokeCommandAction을 선택하고 Command 프로퍼티 오른쪽에 노란색 네모를 클릭해서 Create Data Binding... 메뉴를 선택합니다. 그리고 팝업에서 SwitchThemeCommand를 선택하고 OK를 클릭합니다.

 

화면에서 사용자의 인터렉션(마우스 클릭, 아이템 선택 변경 등)이 발생했을 때 Button이외의 컨트롤은 뷰모델에 변경 사항을 알려주는 방법이 InvokeCommandAction을 이용해서 뷰모델의 ICommand를 호출하는 것입니다. 

 

 

그외 x:Bind를 사용하는 곳도 동일한 방법으로 수정하시면 됩니다.

 

그리고, GridPage에 있던 RadDataGrid를 맨 첫번째 줄에 추가해 주고, 컬럼 4개도 정의를 해줍니다.

여러가지 삽질을 해본 결과 아래와 같은 형태가 제일 무난 한 것 같습니다.

 

전체 소스는 아래 와 같습니다.

 

<Page
    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:ic="using:Microsoft.Xaml.Interactions.Core"
    xmlns:i="using:Microsoft.Xaml.Interactivity"
    xmlns:grid="using:Telerik.UI.Xaml.Controls.Grid"
    xmlns:ViewModels="using:MyPay.ViewModels"
    x:Class="MyPay.Views.SettingsPage"
    mc:Ignorable="d">

    <Page.DataContext>
        <ViewModels:SettingsViewModel />
    </Page.DataContext>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid Margin="{StaticResource MediumLeftRightMargin}">
            <Grid.RowDefinitions>
                <RowDefinition Height="48" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <TextBlock
                Grid.Row="0"
                x:Uid="Settings_Title"
                x:Name="TitlePage"
                Style="{StaticResource PageTitleStyle}" />

            <Grid Grid.Row="1" Margin="0,16,0,0">
                <grid:RadDataGrid ColumnDataOperationsMode="Flyout" UserEditMode="Inline"
                                  UserGroupMode="Disabled" AutoGenerateColumns="False"
                                  ItemsSource="{Binding PayInformations}" HorizontalAlignment="Left">
                    <grid:RadDataGrid.Columns>
                        <grid:DataGridTextColumn Header="Id" PropertyName="Id" CanUserEdit="False" SizeMode="Auto"/>
                        <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>
            </Grid>

            <StackPanel Grid.Row="2" Margin="0,16,0,0">
                <TextBlock
                    x:Uid="Settings_Theme"
                    Style="{StaticResource BodyTextStyle}" />
                <ToggleSwitch
                    x:Uid="Settings_ThemeToggle"
                    IsOn="{Binding IsLightThemeEnabled}"
                    Margin="0,4,0,0">
                    <i:Interaction.Behaviors>
                        <ic:EventTriggerBehavior EventName="Toggled">
                            <ic:InvokeCommandAction Command="{Binding SwitchThemeCommand, Mode=OneWay}" />
                        </ic:EventTriggerBehavior>
                    </i:Interaction.Behaviors>
                </ToggleSwitch>
            </StackPanel>

            <StackPanel Grid.Row="3" Margin="0,16,0,0">
                <TextBlock
                    x:Uid="Settings_About"
                    Style="{StaticResource BodyTextStyle}" />
                <TextBlock
                    Text="{Binding AppDescription, Mode=OneWay}"
                    Style="{StaticResource BodyTextStyle}"
                    Margin="0,4,0,0" />
                <TextBlock
                    x:Uid="Settings_AboutDescription"
                    Style="{StaticResource BodyTextStyle}" />
                <HyperlinkButton
                    x:Uid="Settings_PrivacyTermsLink"
                    Margin="0,8,0,0" />
            </StackPanel>
        </Grid>
    </Grid>
</Page>

 

수정하기 전 화면

 

 

데이터 수정 화면

 

 

데이터 수정시 컨트롤의 너비가 넓어지고 여러가지 버튼들이 추가로 보이는 이유는 터치 기반 UI를 제공하기 때문입니다.

 

 

6. 선택된 탭이 변경될 때마다 커맨드 실행

 

처음에 탬플릿을 만들 때 Tab을 이용한 템플릿을 만들었기 때문에 GridPage와 SettingPage 사이를 이동할 때 Navigation을 타지 않습니다. 그래서, PivotPage에서 SelectedItem을 이용해서 LoadedCommand, UnloadedCommand를 실행 시키도록 합니다.

 

이 작업을 하려면 우선 인터페이스를 하나 추가합니다.

 

Interfaces 폴더를 하나 추가하고, 아래 코드를 이용해서 ILoadedUnloaded.cs 를 추가 합니다.

 

    /// <summary>
    /// ILoadedUnloaded
    /// </summary>
    public interface ILoadedUnloaded
    {
        ICommand LoadedCommand { get; }
        ICommand UnloadedCommand { get; }
    }

 

이 인터페이스를 GridViewModel.cs와 SettingViewModel.cs에 추가해서 인터페이스를 구현 합니다.

 

GridViewModel을 예로 적었습니다. SettingViewModel도 아래 내용을 참고해서 수정하시면 됩니다.

 


    /// 그리드 뷰모델
    /// </summary>
    public class GridViewModel : Observable, ILoadedUnloaded
    {

        /// <summary>
        ///     기본 생성자
        /// </summary>
        public GridViewModel()
        {

            ...

            Init();

        }

        ...

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

 

        public ICommand LoadedCommand { get; private set; }
        public ICommand UnloadedCommand { get; private set; }

    }

 

 

 

 

인터페이스를 사용하기 위해서 PivotPage에 PivotViewModel을 연결하고 Pivot 컨트롤에 SelectedItem 프로퍼티를 바인딩합니다.

 

기존 PivotItem 내부에 <Frame></Frame>은 지워버립니다.

 

<Page
    x:Class="MyPay.Views.PivotPage"
    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:model="using:MyPay.Models"
    xmlns:views="using:MyPay.Views"
    xmlns:viewModels="using:MyPay.ViewModels"
    mc:Ignorable="d">
    <Page.DataContext>
        <viewModels:PivotViewModel />
    </Page.DataContext>

   
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Pivot x:Uid="PivotPage" SelectedItem="{Binding SelectedView, Mode=TwoWay}">
            <PivotItem x:Uid="PivotItem_Grid">
                <views:GridPage/>
            </PivotItem>

            <PivotItem x:Uid="PivotItem_Settings">
                <views:SettingsPage/>
            </PivotItem>

        </Pivot>
    </Grid>
</Page>

 

 

PivotViewModel.cs를 아래와 같이 수정합니다.

 

    /// <summary>
    /// 피벗 뷰모델
    /// </summary>
    public class PivotViewModel : Observable
    {
        private PivotItem _selectedView;

        /// <summary>
        /// 기본 생성자
        /// </summary>
        public PivotViewModel()
        {
        }
 
        /// <summary>
        /// 탭이 변경될 때마다 커맨드 실행
        /// </summary>
        public PivotItem SelectedView
        {
            get => _selectedView;
            set
            {
                if (_selectedView == value) return;
                var view = _selectedView?.Content as FrameworkElement;
                var viewModel = view?.DataContext as ILoadedUnloaded;
                viewModel?.UnloadedCommand.Execute(null);
                Set(ref _selectedView ,value);
                view = _selectedView?.Content as FrameworkElement;
                viewModel = view?.DataContext as ILoadedUnloaded;
                viewModel?.LoadedCommand.Execute(null);
            }
        }
    }

 

이렇게 하고 GridViewModel과 SettingViewModel에 OnLoaded 메소드에 브레이크 포인트를 걸고 테스트를 하면 실행이 되는 것을 볼 수 있습니다.

 

이 작업을 하는 이유는 설정 화면의 경우 OnLoaded 실행 될 때 저장되어 있던 급여 정보를 불러와서 화면에 뿌려주고, OnUnloaded가 실행되면 화면에 정보를 저장하기 위해서 사용됩니다.

 

 

7. 긴급 상황 오류 발생

 

작업 중 아래와 같은 오류가 발생해서 더이상 디자인 타임이 보이지 않습니다. ㅡㅡ;;;

 

해결 방법은 아직 없는 상태 입니다. 알려진 이슈로 수정 중으로 알고 있습니다. 불편하지만 디자인 화면 무시하고 그냥 XAML을 직접 수정하고 실행해서 결과를 확인하도록 하겠습니다. 나중에 이 오류에 대한 해결 방법이 나오면 공유 하도록 하겠습니다.

 

이슈 등록된 내용

https://developercommunity.visualstudio.com/content/problem/48426/xaml-designer-fails-to-render-content-with-message.html

 

System.Runtime.InteropServices.COMException
The deployment operation failed because the specified application needs to be registered first. (Exception from HRESULT: 0x80073D0F)
   at Microsoft.VisualStudio.DesignTools.UwpDesignerHost.AppPackage.AppPackageNativeMethods.IApplicationActivationManager.ActivateApplication(String appUserModelId, String activationContext, ActivateOptions options, Int32& processId)
   at Microsoft.VisualStudio.DesignTools.UwpDesignerHost.AppPackage.AppPackageHelper.ActivateApplication(String appUserModelId, Boolean designerMode, String activationContext, Object site)
   at Microsoft.VisualStudio.DesignTools.UwpDesignerHost.UwpHostPlatform.ActivateApplication(String appUserModelId, Boolean designerMode, String activationContext, Object site)
   at Microsoft.VisualStudio.DesignTools.XamlDesignerHost.Platform.AppContainerProcessDomainFactory.ActivateApplicationInternal(String appUserModelId, String activationContext, Object site)
   at Microsoft.VisualStudio.DesignTools.XamlDesignerHost.Platform.AppContainerProcessDomainFactory.CreateDesignerProcess(String applicationPath, String clientPort, Uri hostUri, IDictionary environmentVariables, Int32& processId, Object& processData)
   at Microsoft.VisualStudio.DesignTools.DesignerContract.Isolation.Primitives.ProcessDomainFactory.ProcessIsolationDomain..ctor(ProcessDomainFactory factory, IIsolationBoundary boundary, AppDomainSetup appDomainInfo, IIsolationTarget isolationTarget, String baseDirectory)
   at Microsoft.VisualStudio.DesignTools.DesignerContract.Isolation.Primitives.ProcessDomainFactory.CreateIsolationDomain(IIsolationBoundary boundary)
   at Microsoft.VisualStudio.DesignTools.XamlDesignerHost.Platform.AppContainerProcessDomainFactory.CreateIsolationDomain(IIsolationBoundary boundary)
   at Microsoft.VisualStudio.DesignTools.DesignerContract.Isolation.Primitives.IsolationBoundary.Initialize()
   at Microsoft.VisualStudio.DesignTools.DesignerContract.Isolation.Primitives.IsolationBoundary.CreateInstance[T](Type type)
   at Microsoft.VisualStudio.DesignTools.DesignerContract.Isolation.IsolatedObjectFactory.Initialize()
   at Microsoft.VisualStudio.DesignTools.DesignerHost.Services.VSIsolationService.CreateObjectFactory(IIsolationDomainFactory isolationDomainFactory, IObjectCatalog catalog)
   at Microsoft.VisualStudio.DesignTools.DesignerContract.Isolation.IsolationService.CreateLease(IIsolationDomainFactory domainFactory)
   at Microsoft.VisualStudio.DesignTools.DesignerContract.IsolatedDesignerService.CreateLease(IIsolationDomainFactory domainFactory, CancellationToken cancelToken, DesignerServiceEntry& entry, IServiceProvider serviceOverrides)
   at Microsoft.VisualStudio.DesignTools.DesignerContract.IsolatedDesignerService.IsolatedDesignerView.CreateDesignerViewInfo(CancellationToken cancelToken)
   at Microsoft.VisualStudio.DesignTools.DesignerContract.Isolation.IsolatedTaskScheduler.InvokeWithCulture[T](CultureInfo culture, Func`2 func, CancellationToken cancelToken)
   at Microsoft.VisualStudio.DesignTools.DesignerContract.Isolation.IsolatedTaskScheduler.<>c__DisplayClass10_0`1.<StartTask>b__0()
   at System.Threading.Tasks.Task`1.InnerInvoke()
   at System.Threading.Tasks.Task.Execute()

 

 

8. 지금까지 작업 내용

 

두개의 화면에 디자인 타임 데이터를 대충 뿌려서 화면 디자인을 완료했습니다. 그나마 천만 다행입니다. 하하;;;

다음에는 로컬 저장소에 급여 정보를 저장하고 불러오는 방법을 추가하고, 앱이 시작하면 그리드에 근무 정보를 입력하면 자동으로 계산이 되도록 기능을 추가해 보도록 하겠습니다.

 

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

내급여 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
UWP 앱 개발 궁금증 해결 포스트  (0) 2017.08.08
Posted by MVP kaki104

댓글을 달아 주세요

내급여 UWP 앱 개발

Part1

Part2

Part3

Part4

 

지난 Part1에 이어서 바로 시작을 하도록 하겠습니다.

 

 

0. 모델에 변경사항이 있습니다.

 

    /// <summary>
    ///     근무 아이템, Observable은 프로퍼티 체인지 이벤트를 발생 시키기 위해서 상속받음
    /// </summary>
    public class WorkItem : Observable
    {
        private DateTime _basicWorkTime;
        private string _description;
        private DateTime _overTime15;
        private DateTime _overTime20;
        private DateTime _overTime25;
        private DateTime _todayWorkTime;

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

        /// <summary>
        ///     근무일
        /// </summary>
        public DateTime WorkDay { get; set; }

        /// <summary>
        ///     휴일여부
        /// </summary>
        public bool IsHoliday { get; set; }

        /// <summary>
        ///     시작시간
        /// </summary>
        public DateTime StartWork { get; set; }

        /// <summary>
        ///     종료시간
        /// </summary>
        public DateTime EndWork { get; set; }

        /// <summary>
        ///     기본 근무 시간 1.0
        /// </summary>
        public DateTime BasicWorkTime
        {
            get => _basicWorkTime;
            set => Set(ref _basicWorkTime, value);
        }

        /// <summary>
        ///     1.5 근무 시간
        /// </summary>
        public DateTime OverTime15
        {
            get => _overTime15;
            set => Set(ref _overTime15, value);
        }

        /// <summary>
        ///     2.0 근무 시간
        /// </summary>
        public DateTime OverTime20
        {
            get => _overTime20;
            set => Set(ref _overTime20, value);
        }

        /// <summary>
        ///     2.5 근무 시간
        /// </summary>
        public DateTime OverTime25
        {
            get => _overTime25;
            set => Set(ref _overTime25, value);
        }

        /// <summary>
        ///     총 근무 시간
        /// </summary>
        public DateTime TodayWorkTime
        {
            get => _todayWorkTime;
            set => Set(ref _todayWorkTime, value);
        }

        /// <summary>
        ///     비고
        /// </summary>
        public string Description
        {
            get => _description;
            set => Set(ref _description, value);
        }

    }

 

 

1. 디자인 타임에 사용할 데이터 생성

 

xaml 의 장점은 디자인 타임에 디자인과 데이터를 함께 볼 수 있다는 점입니다. 그렇게 디자인 타임에 데이터를 보기 위해서는 디자인 타임 데이터를 추가해 주어야 합니다.

 

GridViewModel을 수정합니다. 기본 생성자를 추가한 후 디자인 타임 데이터를 생성해서 Works에 집어 넣습니다.

빌드!!를 꼭 하고 GridPage.xaml에 있는 RadDataGrid와 연결 합니다.

 

    /// <summary>
    /// 그리드 뷰모델
    /// </summary>
    public class GridViewModel : Observable
    {
        private IList<WorkItem> _works;

        /// <summary>
        ///     기본 생성자
        /// </summary>
        public GridViewModel()
        {

            //디자인 타임 데이터 생성
            Works = new List<WorkItem>
            {
                new WorkItem
                {
                    Id = 1,
                    WorkDay = DateTime.Parse("2017-08-23"),
                    StartWork = DateTime.Parse("08:00"),
                    EndWork = DateTime.Parse("19:00"),
                    IsHoliday = true,
                    Description = "test"
                },
                new WorkItem
                {
                    Id = 2,
                    WorkDay = DateTime.Parse("2017-08-24"),
                    StartWork = DateTime.Parse("08:00"),
                    EndWork = DateTime.Parse("19:00"),
                    Description = "what?"
                },
            };


        }
        /// <summary>
        /// 근무 목록
        /// </summary>
        public IList<WorkItem> Works
        {
            get => _works;
            set => Set(ref _works, value);
        }
    }

 

 

2. GridPage.xaml에 디자인 타임 데이터 보이도록 연결

 

바로 연결을 할려고 했는데..일단 코드 비하인드에 필요없는 코드를 제거 하고 정리 합니다.

 

GridPage.xaml.cs

 

    /// <summary>
    ///     그리드 페이지
    /// </summary>
    public sealed partial class GridPage : Page
    {
        public GridPage()
        {
            InitializeComponent();
        }

        /// <summary>
        ///     뷰모델
        /// </summary>
        public GridViewModel ViewModel => DataContext as GridViewModel;
    }

 

그냥 코드를 입력해도 동작은 하지만..그렇게 코드로 입력할려면 뭐하러 비주얼스튜디오를 사용하겠습니까? 툴은 이용하라고 있는 것!!

 

가장 먼저 할 작업은 ViewModel을 DataContext에 집어 넣는 것 입니다. 

 

하나씩 따라해 보시죠

 

페이지를 선택합니다. -> 프로퍼티창에서 DataContext를 찾고 -> New 버튼 클릭 -> Select Object에서 gridview로 검색 -> GridViewModel 선택 -> OK

 

 

위의 작업을 하면 아래와 같은 코드가 추가 됩니다.

 

<Page
    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:tg="using:Telerik.UI.Xaml.Controls.Grid"
    xmlns:ViewModels="using:MyPay.ViewModels"
    x:Class="MyPay.Views.GridPage"
    mc:Ignorable="d">

    <Page.DataContext>
        <ViewModels:GridViewModel/>
    </Page.DataContext>

...

 

뷰모델에 만들어 놓은 Works를 RadDataGrid에 연결 합니다.

 

RadDataGrid를 선택 -> ItemsSource 프로퍼티를 찾고 -> 오른쪽 네모 클릭 -> Create Data Binding... 선택 -> 그림 처럼 나오면 DataContext와 뷰모델이 잘 연결된 것이고, 그렇치 않으면 연결을 다시 해주시면 됩니다. 여하튼! Works를 선택하고 OK를 누릅니다.

 

 

xaml 코드는 아래와 같이 변합니다. 물론 그냥 손으로 쓰셔도 됩니다만.....같이 해야 가치가 있다는..... 쿨럭;;;;

 

        <Grid
            Grid.Row="1"
            Background="{ThemeResource SystemControlPageBackgroundChromeLowBrush}">
            <tg:RadDataGrid ColumnDataOperationsMode="Flyout" x:Name="grid"
                            ItemsSource="{Binding Works}" />
        </Grid>

 

아까 만들어 놓은 디자인 타임 데이터가 보이고 있습니다~ 짜잔~

 

아래처럼 깔끔하게 보이게 하려면 먼저 디자인 타임 해상도를 선택합니다. 저는 12인치 테블릿 화면으로 변경했구요 가로 방향을 선택했습니다. 또한, 확대 비율을 100%로 놓았습니다.

 

 

 

3. 각 컬럼 형태 및 바인딩 하기

 

지금은 WorkItem의 형태대로 각 컬럼이 만들어져서 자동으로 연결되었습니다. 그런데, 실제로 사용하려면 각 컬럼마다 화면에 보이는 형태라든지 입력 방법등을 지정할 수 있어야겠지요?

 

제일 먼저!

 

RadDataGrid를 선택하고 프로퍼티창에서 AutoGenerateColumns 프로퍼티를 찾아 보시면 체크가 되어있을 것입니다. 그 프로퍼티 때문에 바인딩이 되면 자동으로 컬럼을 생성하게 되는 것이니 체크를 해제하겠습니다.

 

 

Part1에서 보면 화면에 첫번째 컬럼은 근무일자였습니다. 그러니 첫번째 컬럼은 날짜 형태로 지정하면 되겠네요.

RadDataGrid를 선택하고 Columns 프로퍼티를 찾고 오른쪽에 ...버튼을 클릭하세요 -> 팝업이 하나뜨는데 거기서 하단부에 콤보박스를 선택해서 Other type...을 선택하시면 Select Object라는 팝업이 출력됩니다.

여기서 컬럼의 타입을 지정할 수 있습니다. DataGridDateColumn을 선택하고, OK를 클릭합니다.

 

 

그러면 이전 팝업으로 돌아오는데 여기서 Add 버튼을 눌러서 DataGridDataColumn을 추가합니다. 추가를 하고 CellContentFormat에 {0:d}라고 입력, Header에 근무일자, PropertyName에 WorkDay라고 입력을 하고 OK를 클릭합니다.

 

컬럼이 추가되자마자 바로 데이터가 보이면 좋은데 아쉽게도 빌드를 한번 해주어야 보입니다. 빌드를 살짝 한번 눌러주세요

 

 

이쯤하고 실행을 한번 해보겠습니다.

음..날짜는 나오는데..헤더에 한글이 나오지 않습니다.;; 이힝..지금 한글 나오지 않는 것 때문에 한시간 가량 삽질을 했는데.. 해결이 되지 않아..이슈로 등록해 놓았습니다. 뭐라고 이야기를 해주겠죠;; 일단은 영문으로 갑니다.

 

 

위의 방법으로 몇개의 컬럼을 더 추가해 보겠습니다. 방법은 동일하며, bool형태와 택스트 형태를 추가합니다.

 

            <tg:RadDataGrid ColumnDataOperationsMode="Flyout" x:Name="grid"
                            ItemsSource="{Binding Works}" AutoGenerateColumns="False">
                <tg:RadDataGrid.Columns>
                    <tg:DataGridDateColumn PropertyName="WorkDay" Header="Work Day" CellContentFormat="{}{0:d}" />
                    <tg:DataGridBooleanColumn Header="Holiday" PropertyName="IsHoliday" />
                    <tg:DataGridTextColumn Header="Description" PropertyName="Description" />

                </tg:RadDataGrid.Columns>
            </tg:RadDataGrid>

 

실행을 해보니 이렇게 나옵니다~

 

 

 

시간이 늦어서 오늘은 이만하도록 하겠습니다. 다음에 봐용

 

Posted by MVP kaki104
TAG UWP

댓글을 달아 주세요

  1. 2017.08.24 09:32  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

  2. 2017.09.20 16:05 Gwang  댓글주소  수정/삭제  댓글쓰기

    항상 올려주시는 좋은 정보들 감사합니다!
    질문사항이 있어 코맨트 남깁니다.
    위 한글 깨짐현상 해결하셧나요?
    저같은 경우, 간단하게

  3. 2017.09.20 16:06 Gwang  댓글주소  수정/삭제  댓글쓰기

    ;;; 갑자기 글이 올라가버렸네요.
    간단하게 버튼을 생성하는 것에서도 한글이 깨져서 나오지않았습니다.
    혹시 해결을 하셨다면 해결방법이 궁금합니다!

    • 2017.09.24 12:38 신고 MVP kaki104  댓글주소  수정/삭제

      일반 버튼은 한글이 깨지지 않을 것 같습니다만..저도 테스트를 해보겠습니다.
      -> 테스트 결과 정말 앙되는 군요..
      음..아무래도 코드 문제인 듯 한데..
      현재 해결 방법은 Resource 문자열에 한글을 넣어서 사용하는 것으로 해결이 가능하기는 합니다.
      더 자세한 사항은 part6 포스트에서 다루도록 하겠습니다.

지난번 포스트에서 Windows Template Studio를 이용해서 앱 탬플릿을 간단하게 만들 수 있는 것을 확인 했습니다. 하지만.. 탬플릿은 템플릿일 뿐!! 진짜 만들어 봐야 응가인지 아닌지 알 수 있겠죠?

 

특히, 해당 템플릿을 사용하면 Telerik 컨트롤들을 사용할 수 있는데, 예전부터 Telerik에서 나온 컨트롤들을 이용해서 WPF, Silverlight 프로젝트를 진행했던 과거가 있어서, UWP에서는 어떤 컨트롤들이 있는지 궁금했습니다. 이번 기회에 궁금증도 풀겸 간단한 앱을 하나 만들어 보려고 합니다.

 

0. 참고 포스트

Windows Template Studio를 이용해서 UWP 앱 개발 시작하기

http://kaki104.tistory.com/546

 

기본 환경 및 Windows Template Studio에 대한 설명은 이 포스트를 참고하세요

 

 

1. 시작하기

 

화면은 크게 2개입니다. 하나는 근무 시간 입력화면이고 다른 하나는 설정 화면입니다.

 

근무 시간 입력화면에 사용될 컨트롤은 Telerik사의 RadDataGrid를 사용할 예정입니다.


RadDataGrid Overview

http://docs.telerik.com/windows-universal/controls/raddatagrid/overview

 

 

 

 

2. 프로젝트 만들기

 

비주얼스튜디오 2017을 실행 -> File -> New -> Project

 

Windows Template Studio (Universal Windows)를 선택 -> MyPay 입력 -> 폴더 선택 -> OK

 

 

Pivot and Tabs선택, MVVM Basic 선택 후 Next 클릭

 

 

Settings + 버튼을 눌러서 설정 화면 추가, Grid + 버튼 눌러서 그리드 추가 후 Create 버튼을 눌러서 프로젝트를 생성합니다.

 

 

생성된 프로젝트를 실행하고, Grid를 선택하면 아래와 같은 화면이 나옵니다.

이제 진짜로 손을 보면서 만들어 보겠습니다.

 

 

 

3. Main 삭제하기

 

Main 페이지는 아무런 내용이 없는 페이지이기 때문에 필요 없습니다. 삭제를 하도록 하겠습니다.

 

PivotPage.xaml을 선택한 후 PivotItem 중 MainPage가 있는 부분을 삭제 합니다.

 

MainPage.xaml을 선택한 후 Delete키를 눌러서 삭제 합니다.

 

 

다시 실행을 하면 Grid와 Settings만 보입니다.

 

 

 

4. RadDataGrid 디자인 타임에 발생하는 오류 처리

 

RadDataGrid를 선택하고 뭐좀 해볼려고 했더니, 바로 디자인 타임에 오류가 발생합니다. 찾아보니 Update를 해주면 해결이 되네요..

 

 

프로젝트를 선택하고 마우스 오른쪽 클릭해서 컨텍스트 메뉴를 출력하고 Manage NuGet Packages...를 선택합니다.

 

 

Telerik.UI.for.UniversalWindowsPlatform 선택 -> Versions에서 1.0.0.5를 선택하고 Update 버튼을 클릭해서 업데이트를 진행 합니다. 참고로 기존 버전은 1.0.0.4입니다.

 

설치가 완료된 후 비주얼스튜디오를 재시작하고 빌드를 하면 정상적으로 표시 됩니다.

 

 

 

5. 모델 만들기

 

Models 폴더에 WorkItem이라는 클래스를 생성합니다.

 

using System;
using MyPay.Helpers;

namespace MyPay.Models
{
    /// <summary>
    ///     근무 아이템, Observable은 프로퍼티 체인지 이벤트를 발생 시키기 위해서 상속받음
    /// </summary>
    public class WorkItem : Observable
    {
        private DateTime _basicWorkTime;
        private DateTime _overTime15;
        private DateTime _overTime20;
        private DateTime _overTime25;

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

        /// <summary>
        ///     근무일
        /// </summary>
        public DateTime WorkDay { get; set; }

        /// <summary>
        ///     휴일여부
        /// </summary>
        public bool Isholiday { get; set; }

        /// <summary>
        ///     시작시간
        /// </summary>
        public DateTime StartWork { get; set; }

        /// <summary>
        ///     종료시간
        /// </summary>
        public DateTime EndWork { get; set; }

        /// <summary>
        ///     기본 근무 시간 1.0
        /// </summary>
        public DateTime BasicWorkTime
        {
            get => _basicWorkTime;
            set => Set(ref _basicWorkTime, value);
        }

        /// <summary>
        ///     1.5 근무 시간
        /// </summary>
        public DateTime OverTime15
        {
            get => _overTime15;
            set => Set(ref _overTime15, value);
        }

        /// <summary>
        ///     2.0 근무 시간
        /// </summary>
        public DateTime OverTime20
        {
            get => _overTime20;
            set => Set(ref _overTime20, value);
        }

        /// <summary>
        ///     2.5 근무 시간
        /// </summary>
        public DateTime OverTime25
        {
            get => _overTime25;
            set => Set(ref _overTime25, value);
        }

        /// <summary>
        ///     총 근무 시간
        /// </summary>
        public DateTime TodayWorkTime { get; set; }
    }
}

 

 

 

오늘은 여기까지만 하겠습니당~ 너무 많이 하면 힘드니까요~ 하하;;

 

Posted by MVP kaki104

댓글을 달아 주세요

지난주에 (주)CLONIX 대표님이 UWP앱 개발에 대해서 문의를 주셔서 간단학[ 한번 미팅을 했습니다. 그 미팅으로 어느정도 방향을 잡으시고, 셈플 프로젝트를 만들어 보신 후에 추가 문의 사항을 해결하기 위해, 8월 7일에 토즈 수내 스터디 센터에서 3시간 가량 강의를 진행 했으며, 그 중 몇가지 내용을 후기식으로 적어 보도록 하겠습니다.

 

 

1. Windows Template Studio를 이용해서 앱을 생성했을 때 버튼 스타일을 변경하기..

 

http://kaki104.tistory.com/546 포스트에서 Windows Template Studio를 이용해서 기본 앱을 빨리 만들 수 있는 방법을 소개했었습니다.

 

포스트를 쓰기 위해서 제가 전에 만들었던 앱에 버튼을 하나 올리고 스타일을 수정하기 위해 Edit Template -> Edit a Copy ...를 선택하니 정상적으로 스타일이 생성됩니다. ^^;;; 어제는 앙됐는데..쿨럭

 

이 작업은 블랜드로 하셔야 합니다. 나중에 States 탭을 사용해야 하기 때문입니다.

 

버튼에서 마우스 오른쪽 클릭 -> Edit Template -> Edit a Copy...

 

 

OK를 클릭해서 Page에 ButtonStyle1이라는 이름으로 스타일을 생성합니다.

 

 

버튼을 눌렀을 때 배경색을 변경해 보도록 하겠습니다.

일단 왼쪽 상단 탭 중 States 탭을 클릭합니다. 그러면 Normal, Pressed, Disabled, PointerOver라는 각 상태별로 스타일을 변경 할 수 있는 화면이 나옵니다.

 

Pressed 선택

* 타임라인 아이콘을 찾아서 누르시면 좀더 자세히 볼 수 있습니다.

* 상태를 선택하면 자동으로 레코딩 모드로 돌입합니다.(주위가 뻘건색으로 변하는 것을 이야기 합니다.) 

 

 

버튼의 배경색은 RootGrid의 배경색에 따라서 변경됩니다. ContentPresenter는 배경색 프로퍼티를 지정하려고 하면 오류가 발생합니다. 글씨색은 ContentPresenter의 Foreground를 변경해서 바꿀 수 있습니다.

 

RootGrid선택 -> Background 선택 -> SolidColorBrush 선택(마우스 오버하면 글씨 보입니다.) -> 컬러 입력창에 yellow를 입력하면 자동으로 #FFFFFF00 이렇게 변경 됩니다.

 

 

이렇게 PointerOver일 때도 컬러를 변경해 주시면 됩니다.

 

 

2. App Service를 이용해서 Win32 프로그램과 통신하기

 

AppService Bridge Sample 프로젝트를 이용해서 어떻게 하는 것인지를 살펴 보면서, 기존 앱을 어떻게 이렇게 만들 수 있는지 설명을 드렸내요

 

프로젝트 Reference를 보면 Windows Desktop Extensions for the UWP가 추가되어 있는 것을 볼 수 있습니다.

App Service 자체는 앱과 앱 사이의 통신을 하기 위한 기능이기 때문에 필요 없지만, Win32 app을 실행 하기 위해서는 이 Extensions이 반드시 필요합니다.

 

음 아실런지 모르지만, 사실 UWP는 샌드박스이기 때문에 Win32 app을 실행 시킬수도, 로컬 폴더에 직접 엑세스 할 수도 없습니다. 하지만, 이제는 Win32 app을 실행 시킬 수 있습니다. 단!! 앱과 함께 배포가된 exe 파일만 실행 가능 합니다~

 

 

실행 시킬 파일 이름은 어디에 입력할까요?

 

Package.appxmanifast 파일 선택 -> 마우스 오른쪽 버튼 클릭 -> Open With... -> XML (Text) Editor -> OK

 

 

Exeutable="여기에 exe 파일명을 입력하시면 됩니다."

 

 

 

그럼 exe파일은 어디에 있어야 하나요?

 

C:\Users\MunChan Park\Downloads\DesktopBridgeToUWP-Samples-master\DesktopBridgeToUWP-Samples-master\Samples\AppServiceBridgeSample\cs\UWP\bin\x86\Debug\AppX

 

저는 대략 이 위치에 있습니다.

 

 

그럼 진짜로 실행 시키는 방법은??

 

AppServiceBridge.xaml.cs 파일에

 

await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();

 

문장을 통해서 실행이 됩니다.

 

 

3. 실행

 

Launch Background Process 버튼을 눌러서 Win32 app을 실행 시키고, hello를 입력하고 Send Request to Background Process 버튼을 클릭하면 win32 app으로 전송 후 대문자로 변환된 HELLO를 반환 받아서 출력합니다.

 

 

 

여기서 살짝 고쳐서 WPF로 만든 앱을 실행 시켜 보았습니다.

WPF app에서 폴더 선택 창을 출력해서 아무 폴더나 선택해서 파일을 쓰는 작업까지 가능한 것을 확인 했습니다.

 

 

 

4. 마무리

 

회의실 사진을 찍었어야 하는데..대신 영수증을 첨부합니다. ㅎㅎ 저도 이번에 App Service에 대해서 간단하게 맛을 볼 수 있어서 좋았습니다.

UWP 개발에 관심이 있는 회사와 관계자 분들의 문의를 환영 합니다.~

 

 

Posted by MVP kaki104

댓글을 달아 주세요

지난번 MS행사에서 발표한 내용중에 하나로 UWP앱을 좀더 쉽게 만들기 위해 발표된 템플릿입니다.

이제 버전도 정식 버전이고, 알려드릴 때가 된 것 같아서 포스트를 하게 되었습니다.

시작하기에 앞서서 먼저 어떤 녀석인지 살짝 화면을 보여드리면..

 

프로젝트 타입을 선택하는 화면 입니다.

 

 

선택한 프로젝트에 원하는 형태의 페이지를 추가하는 화면 입니다.

 

 

이렇게 프로젝트 타입과 페이지들을 마법사 화면으로 추가할 수 있도록 되어있는 템플릿입니다.

시작이 반이니 일단 멋진 UWP이 순식간에 만들어지니 개발 의욕이 쑥쑥~ 올라가겠죠?

 

 

1. 준비사항

 

기본준비는 이 페이지를 참고하세요 https://developer.microsoft.com/ko-kr/windows/downloads

 

* Visual Studio 2017 업데이트1 이상의 버전이 필요합니다.

* 윈도우 10 Creators Update SDK 또한 필요합니다.(version 10.0.15063.0 or later)

* 윈도우 버전 또한 Creators Update가 적용되어 있어야 합니다.(문서에는 없었지만..SDK버전에 맞는 윈도우가 필요할 것이라 생각됩니다.)

 

 

2. Windows Template Studio를 설치하기

비주얼 스튜디오를 실행하시고,

 

Tool -> Extensions and Updates -> Online -> template 혹은 Windows template를 입력해서 검색을 하신 후 Download를 선택하시면 됩니다.

 

 

비주얼 스튜디오 2017부터는 확장 기능은 비주얼 스튜디오를 종료해야 설치가 가능합니다. 그러니, 바로 종료를 해줍니다. 잠시만 기다리시면, 인스톨러가 실행되고, Modify를 선택해 줍니다. 설치가 완료되면 Close 버튼을 누른 후 비주얼 스튜디오를 다시 시작합니다.

 

 

 

3. 프로젝트 만들기

 

File -> New -> Project -> Windows Universal -> Windows Template Studio (Universal Windows)를 선택하시고

앱 이름을 입력하고 OK를 클릭합니다.

 

 

프로젝트 타입과 프레임웍을 선택하는 창이 출력됩니다. 오호~ 뭔가 있어 보입니다. 참고로, 번호는 세로 우선으로 이야기 합니다.

 

프로젝트 타입

1. 네비게이션 타입

2. 피벗과 탭 타입

3. 빈 화면

 

프레임웍

1. 코드 비하인드 : 일반적인 개발 방법인 이벤트 드리븐 방식으로 개발을 하고자 할 때 사용

2. MVVM Basic : 기본적인 MVVM 패턴을 이용하는 방법

3. MVVM Light : MVVMLight라는 프레임웍을 이용하는 방법

 

저는 1번과 2번을 선택하도록 하겠습니다.

 

페이지 종류가 나옵니다.

 

1. 빈페이지

2. 멀티미디어 플레이어

3. 차트

4. 설정

5. 마스터/디테일

6. 탭

7. 웹뷰

8. 그리드

9. 맵

 

상당히 여러 종류가 지원되네요 모두 하나씩 추가를 하도록 하겠습니다.

 

 

어플리케이션 라이프사이클

1. 설정 저장소

2. 서스팬드와 리줌

 

처음 만드는 프로젝트이니 이녀석들은 추가하지 않도록 하겠습니다.

 

 

백그라운드 워크

1. 백그라운드 테스크

 

백그라운드 테스크도 추가하지 않도록 하겠습니다.

 

 

사용자 인터렉션

1. 토스트 노티피케이션

2. 라이브 타일

3. 에저 노티피케이션

4. 퍼스트 런 프롬프트

5. 데브 센터 노티피게이션

6. 왔즈 뉴 프롬프트

 

이름만 봐서는 뭔지 모르겠는 녀석들도 있네요..음..대충봐도 알만한 토스트 노티피케이션과 라이블 타일을 추가하겠습니다.

 

모두 선택하신 후 Create 버튼을 눌러주시면 혼자서 프로젝트를 열심히 생성합니다.

 

 

4. 완성된 프로젝트 템플릿 살펴 보기

 

프로젝트가 완성되는데 한 2~3분 정도 소요된 것 같습니다. 솔루션 탐색기를 보니 폴더가 마니 생겼네요.

F5를 눌러 실행을 하도록 하겠습니다.

 

 

 

실행하는데 에러 발생하지 않고, 잘 실행됩니다. 하하..

* UWP 앱 프로젝트를 만드시면 윈도우 설정에 개발자 모드가 활성화 되어 있어야지만 만들 수 있습니다. 참고하세요

 

햄버거 버튼이 보이는 아주 기본적인 화면이네요 아까 추가한 8개의 페이지가 차례대로 표시되고 있습니다. 이정도에서 놀라면 앙되겠죠? 각 페이지를 자세히 살펴 보도록 하겠습니다.

 

 

미디어 플레이어 페이지를 선택하자마자 채널9인 동영상이 재생됩니다. 플레이어 모습도 Movie앱과 매우 유사한 것이 좋아 보이네요

 

차트 페이지 입니다.

간단한 바 차트가 출력됩니다. 음 아무래도 텔레릭 차트일 것 같은 느낌이 드네요

 

 

마스터 디테일 페이지 입니다.

 

왼쪽 아이템을 선택하면 오른쪽에 상세 데이터를 볼 수 있습니다. 그런데, 화면 사이즈를 줄이니...

 

 

깜찍하게 요렇게 변경됩니다. 여기서 아이템을 선택하면 상세 페이지가 출력됩니다. 그리고 원래 화면으로 돌아오려면 Back 키를 누르거나 <- 버튼을 눌러 줍니다. 이런게 코딩 하나 않하고 다 구현이 되어 있군요..후후

 

 

탭 화면 입니다.

 

 

웹뷰 페이지 입니다.

음..네이뻐 페이지로 가보고 싶은데..주소 입력하는 창이 업어서 못가겠네요..ㅎㅎ

 

 

그리드 페이지 입니다.

헉!!!! 대박이네요. 이건 완전 텔레릭 컨트롤인데요.. 그냥 사용이 가능한가봅니다. 뭐 물론 약간의 기능상 제약이 있을 것 같기는 하지만, 이정도만 해도 기본 앱 만드는데는 전혀 문제가 없어 보입니다.

 

 

맵 페이지 입니다.

제가 있는 동네를 표시하고 있습니다. 흐흐흐 지도 컨트롤에 한국 지도 나오는건 다들 알고 계시죠? 이제 적극 활용할 수 있을 것 같습니당~

 

 

설정 창입니다.

 

테마를 Light에서 Dark로 변경해 보았습니다. 잘되네요..후후후후

 

 

앱을 시작 페이지에 추가했더니, 라이브 타일이 변경되는 것을 볼 수 있었습니다.

 

 

토스트 노티피케이션은 앱이 시작할 때 발생합니다.

 

 

5. 이정도면 큰 힘들이지 않고도 간단한 앱은 만들 수 있을 것 같습니다.

 

 

Posted by MVP kaki104

댓글을 달아 주세요

  1. 2017.08.04 09:23  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

지난 포스트에서 Template10(이하 T10)에 대한 간단한 소개를 했습니다. 이번부터는 본격적으로 어떤 내용들이 있는지 살펴 보려고 합니다. 그 중에서 가장 먼저 T10에서만 사용가능한 Controls들에 대해서 살펴 보도록 하겠습니다.


MVVM Pattern을 사용하는 개발자를 위한 안내


Template10을 이용해서 UWP 앱 개발하기 Part1



1. 참고


Template10

MVVM



2. Template10에 적용된 MVVM 알아보기


Template10은 MVVM Pattern을 이용해서 앱을 만들기 위한 기본적인 내용들을 포함하고 있습니다. 또한, 그러한 내용은 Override를 통해서 사용자가 원하는 스타일로 변경할 수도 있습니다.



3. BindableBase


이 클래스는 Binding에 기본이되는 INotifyPropertyChanged 인터페이스를 상속 받았으며, 값이 변경된 것을 알려야 할 필요가 있는 프로퍼티에 다음과 같은 형태로 이용할 수 있습니다.


private string _Value = "Default";
public string Value
{
    get
    {
        return _Value;
    }
    set
    {
        Set(ref _Value, value);
    }
}



4. DelegateCommand


이 클래스는 사용자의 Interaction을 ViewModel에 전달하는 역할을하는 ICommand 인터페이스를 상속 받았습니다. 버튼 컨트롤에 Command와 바인딩을 하는 커맨드를 생성할 때 다음과 같은 형태로 사용할 수 있습니다.


DelegateCommand _SaveCommand;

public DelegateCommand SaveCommand => _SaveCommand ?? (_SaveCommand = new DelegateCommand(ExecuteSave, CanSave));

private void ExecuteSave()
{
    _dataService.Save();
}

private bool CanSave()
{
    return _dataService.IsValid();
}


커맨드를 만드는 형태는 여러가지가 있을 수 있습니다. 음..제가 자주 사용하는 방법은 ICommand를 프로퍼티 타입으로 선언하는 방법인데,  이렇게 하면 CommandParameter 존재 여부를 구분하지 않아도 되서 편리하기 때문입니다.



5 ViewModelBase


이 클래스는 페이지 네비게이션을 뷰모델에서 사용하기 위한 INavigable 인터페이스, BinadableBase를 상속 받았습니다. 또한, 각 페이지의 세션 데이터를 저장할 수 있는 기능도 제공하고 있으며, 사용자가 ViewModelBase를 상속 받아 새로운 클래스를 만들어서 사용하면, 원하는 기능을 추가할 수도 있습니다.


public virtual IDispatcherWrapper Dispatcher { get; set; }
public virtual INavigationService NavigationService { get; set; }
public virtual IStateItems SessionState { get; set; }

public virtual Task OnNavigatingFromAsync(NavigatingEventArgs args)
public virtual Task OnNavigatedFromAsync(IDictionary<string, object> pageState, bool suspending)
public virtual Task OnNavigatedToAsync(object parameter, NavigationMode mode, IDictionary<string, object> state)



6. MVVM Frameworks


다른 MVVM 프레임 워크를 활용하려면 Template10.Mvvm.ViewModelBase에서 상속 받거나 Template10.Services.NavigationService.INavigable을 구현하면 OnNavigatedTo / From을 뷰모델에서 사용할 수 있습니다.


Posted by MVP kaki104

댓글을 달아 주세요

2017년 새해가 밝아 왔습니다. 하지만, 아직까지도 MVVM Pattern 지역은 그렇게 밝게 빛나지 않고 있는 것 같습니다. 1월 14일에 부산에서 열리는 Microsoft UWP(Universal Windows Platform) App 개발 세미나에서 설명을 위해서 사용할 MVVM Pattern(이하 MP로 사용하기도 합니다)을 개발자를 위한 안내 포스트를 작성하려고 합니다.


0. 참고

The MVVM Pattern


Todo list Universal & UWP app


MVVM pattern 설명 동영상(오래전에 녹화한 내용입니다.)
http://youtu.be/f9aQkuoiPz4
http://youtu.be/uGxboAUwciI
http://youtu.be/2lQQiBEjbtU


Using MVVM Pattern




1. MVVM Pattern의 시작

MVVM은 WPF (Windows Presentation Foundation) 및 Silverlight의 기능을 활용하여 사용자 인터페이스의 이벤트 중심 프로그래밍을 간소화하기 위해 Microsoft 아키텍처 Ken Cooper 및 Ted Peters가 개발했습니다. 그리고, Microsoft의 WPF 및 Silverlight 아키텍트 중 한 명인 John Gossman은 2005 년 자신의 블로그에서 MVVM을 발표했으며, 벌써 12년이 지났습니다. 하지만, 아직까지 국내에서는 MP에 대한 인지율이 낮고, 제대로 알지 못한 상태에서 사용하여 프로젝트가 산으로 가는 경우가 많은 것 같습니다.



2. kaki's MVVM Pattern

처음 MVVM Pattern을 접한 것이 2010년 10월 교육센터에서 였으며, 그 때부터 지금까지, WPF, Silverlight, Windows app 개발 프로젝트를 진행하였고, 2016년에는 S사에서 Flex로 개발된 솔루션을 WPF로 컨버전하는 프로젝트에서 아키텍처를 담당하였습니다. Prism 5.0을 기반으로 MVVM Pattern, Telerik UI Control을 이용해서 프레임웍을 구축하고, Custom Control, 공통 모듈 개발을 진행했습니다.



3. MVVM Pattern에 대한 Q&A

Q. MVVM Pattern을 꼭 사용해야 하나?

A. MP는 WinForm이나 ASP.Net 개발에서는 사용할 수 없으며, 오직, WPF, Silverlight, UWP app 개발에서만 사용이 가능합니다. 만약, WPF로 개발하는데 MP를 사용하지 않는다고 하면, 그냥 WinForm 프로젝트로 변경해서 진행하는 것이 더 효과 적이라고 생각합니다. 결과적으로는 WPF, Silverlight, UWP app 실무 프로젝트 개발은 MP를 사용해야 한다고 보시면 됩니다.


Q. 코드 비하인드(MainPage.xaml.cs)에 코딩을 하지 못하나?

A. MP의 기본 방향은 View와 ViewModel의 분리입니다. 그렇기 때문에 될 수 있으면, 코드 비하인드에 직접 코딩을 하지는 않습니다. 하지만, Control에 내장되어있는 Method를 호출해야하는 경우라든지, View에 붙어서 해야하는 작업인 경우에는 코드 비하인드에 직접 코딩을 하는 것이 더 효율적입니다. 이렇게 작업을 하는 경우에는 메모리 누수가 발생하지 않도록 추가적으로 코딩을 해주는 것이 중요합니다.


Q. 개발 속도가 느리다.

A. MVVM Pattern의 목적은 개발 속도 향상이 아닌 유지보수의 비용 감소에 있습니다. 그렇기 때문에 WinForm 개발 속도와 비교를 할 수는 없습니다. 정말 빠르게 개발하고 완료를 해야하는 프로젝트라면, WinForm 프로젝트로 진행하는 것을 권장 합니다. 다만, 개발 완료 후 다년간 성능 개선과 유지보수를 진행한다고 했을 때는 제대로 만들어진 MP 프로젝트가 WinForm 프로젝트보다는 적은 리소스가 들어간다고 할 수 있습니다. 특히! Unit Test를 도입을 한다면, 유지보수에 들어가는 비용은 더욱더 줄어들 것입니다.


Q. 뷰 혹은 컨트롤을 뷰모델에 바인딩해서 사용해도 되나요?

A. MP의 기본은 뷰와 뷰모델을 서로 직접 연결하지 않는 것에 있습니다. 그러므로, 뷰 혹은 컨트롤을 직접 바인딩해서 뷰모델에서 프로퍼티를 변경하거나, 조작하는 방식은 사용하지 말아야 합니다. 만약 이렇게 사용하게 되면 메모리 누수의 직접적인 원인이 되어, 차후에 문제가 발생합니다.



4. MVVM Pattern의 핵심 개념

1) Model : 데이터를 처리하는 기본 단위로 데이터 클래스를 이야기 합니다.

2) View : 사용자가 보고, 입출력하는 화면으로 xaml 파일을 이야기 합니다

3) ViewModel : View의 추상화 클래스로, 비지니스 로직이 구현되어 있습니다.





5. UWP 앱을 예를 들어서 설명을 하도록 하겠습니다.


Template10을 이용한 UWP Blank app 프로젝트를 만든 후 버튼을 하나 추가했습니다.

버튼을 클릭하면 Hello UWP World라는 글씨로 변경됩니다.






1) MVVM Pattern을 사용하지 않는 일반적인 event-driven 방식을 이용해서 변경되는 과정을 보겠습니다.


화면에 Hello World라는 글씨가 나오는 부분과 버튼에 해당하는 부분의 MainPage.xaml 코드를 보겠습니다.


        <RelativePanel EntranceNavigationTransitionInfo.IsTargetElement="True"
                       RelativePanel.AlignBottomWithPanel="True"
                       RelativePanel.AlignLeftWithPanel="True"
                       RelativePanel.AlignRightWithPanel="True"
                       RelativePanel.Below="pageHeader">

            <!--  content  -->
            <TextBlock x:Name="textBlock"
                       Margin="16,12,0,0"
                       Text="Hello World" />
            <Button Content="Change Text" RelativePanel.Below="textBlock" Margin="10"
                    Click="Button_Click"/>
        </RelativePanel>


. TextBlock 컨트롤의 Text 프로퍼티에 "Hello World"라는 글씨가 입력되어 있습니다. 앱을 실행하면 바로 이 내용이 화면에 출력됩니다.

. 그 컨트롤 아래 Button 컨트롤의 Content라는 프로퍼티에 "Change Text"라는 글씨를 넣어 놓았습니다. 또한, 버튼을 클릭하면 Button_Click이라는 코드비하인드에 있는 메소드를 실행하게 됩니다.



MainPage.xaml.cs 코드 비하인드에 있는 Button_Click이라는 메소드의 내용을 살펴보겠습니다.


        private void Button_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            textBlock.Text = "Hello UWP World";
        }


textBlock.Text라는 곳에 "Hello UWP World"라는 문자열을 입력하도록 되어있습니다. 즉, textBlock이라는 컨트롤의 Text 프로퍼티의 값을 직접 변경함으로 화면에 출력되는 내용이 변경됩니다.


위의 예제에서 Button 컨트롤의 Click 이벤트를 이용해서 어떤 작업을 하도록 지시를 했기 때문에 이런 처리 방식을 event-driven이라고 이야기 합니다. 또한, 화면을 구성하는 MainPage.xaml 파일에 Click="Button_Click"이라고 입력된 내용과 MainPage.xaml.cs에 Button_Click 메소드는 서로 직접 연결이 되어 있기 때문에 tightly coupled 연결이라고 이야기 합니다.


* event-driven 정리

이 방식에서 필요한 것은 딱 2가지 입니다. 화면과 코드 비하인드..그만큼 알아야 할 내용도 추가적으로 공부할 내용도 없는 매우 간단한 구조입니다.


그런데 이렇게 간단한 구조이지만, 프로젝트의 규모가 커지고 투입 인원이 늘어나고, 유지보수 기간이 늘어 날 수록 여러가지 문제가 발생하게 됩니다. 메모리 누수, 패턴이 없기 때문에 개발자마다 코딩 방법이나 표현 방법이 달라 유지보수에 리소스가 많이 소비됨, 성능문제 등이 가장 대표적인 문제 입니다.



2) MVVM Pattern으로 변경해 보겠습니다.


MainPage.xaml를 보겠습니다.


        <RelativePanel EntranceNavigationTransitionInfo.IsTargetElement="True"
                       RelativePanel.AlignBottomWithPanel="True"
                       RelativePanel.AlignLeftWithPanel="True"
                       RelativePanel.AlignRightWithPanel="True"
                       RelativePanel.Below="pageHeader">

            <!--  content  -->
            <TextBlock x:Name="textBlock"
                       Margin="16,12,0,0"
                       Text="{Binding HelloText}" />
           
            <Button Content="Change Text" RelativePanel.Below="textBlock" Margin="10"
                    Command="{Binding TextChangeCommand}"/>
        </RelativePanel>


. TextBlock 컨트롤에 Text 프로퍼티에 "{Binding HelloText}"라고 되어 있습니다. MP에서 가장 중요한 Binding이라는 기술을 이용해서 ViewModel에 있는 HelloText 프로퍼티를 연결해 놓은 것입니다.

. Button 컨트롤에 Command라는 프로퍼티에 "{Binding TextChangeCommand}"라고 되어 있습니다. Button은 기본적으로 Command라는 프로퍼티를 가지고 있으며, Click 이벤트가 발생하면 Command 프로퍼티와 연결되어 있는 ViewModel에 TextChangeCommand라는 커맨드를 실행합니다.


위에서 사용한 Binding이란 개념과 Command에 대해서 잠시 살펴 보겠습니다.


* Binding은 Data Binding을 이야기 한 것으로, ViewModel에 특정 프로퍼티와 컨트롤의 특정 프로퍼티를 서로 연결해서 ViewModel의 프로퍼티의 값이 변경되면, 자동으로 컨트롤의 프로퍼티 값도 변경되는 형태를 이야기 합니다.

여기서 프로퍼티의 값이 변경되었을 때 바인딩 대상에서 알리는 방법은 INotifyPropertyChanged 인터페이스를 이용합니다.

* UWP 앱에서는 Binding 문으로 사용하는 duck type 바인딩과 x:Bind 문으로 사용 non duck type 바인딩을 사용할 수 있으며, 더 자세한 사항은 데이터 바인딩 개요 페이지를 참고하시면 됩니다.



* Command는 화면에서 발생하는 사용자의 Interaction을 ViewModel에 전달하는 중간자 역할을 합니다. 명령을 실행 시키는데 필요한 Parameter를 전달하기 위해서 CommandParameter라는 프로퍼티도 사용할 수 있습니다.



MainPageViewModel.cs 코드를 보겠습니다.


    public class MainPageViewModel : ViewModelBase
    {
        private string _helloText;

        public MainPageViewModel()
        {
            Init();
        }

        private void Init()
        {
            HelloText = "Hello World";

            TextChangeCommand = new DelegateCommand(() =>
            {
                HelloText = "Hello UWP World";
            });
        }

        public string HelloText
        {
            get { return _helloText; }
            set { Set(ref _helloText ,value); }
        }

        public ICommand TextChangeCommand { get; set; }
    }


처음에 우리가 보았던 화면은 Hello World라는 글씨가 출력되어 있었고, 버튼을 눌러서 글씨를 변경하는 내용이 였습니다. 이 중 비지니스 로직만 분리를 하면,


- Hello World라는 글씨가 처음부터 출력되어 있어야 함

- 버튼을 누르면 Hello UWP World라는 글씨로 변경한다.


입니다. 이 2가지의 기능만을 수행하는 별도의 클래스를 ViewModel이라고 부르게 되는 것입니다.


뷰모델의 코드를 보면 HelloText라는 프로퍼티는 뷰모델이 생성이 되면서 Hello World라는 문자열 값을 가지게 되고, TextChangeCommand가 실행되면, Hello UWP World라는 문자열로 변경됩니다. 이때 Set(ref _helloText, value) 문장에서 기존 문자열 값과 새로 들어온 값이 서로 다르다면, NotifyPropertyChanged 이벤트가 발생됩니다. 그러면, 뷰에 HelloText 프로퍼티와 Binding되어있던, textBlcok의 Text 프로퍼티의 값이 변경되면서, 바뀐 문자열이 표시 됩니다.



3) 그렇다면.. 여기서 궁금한 것이 있습니다. 어떻게 뷰모델과 뷰가 서로 연결되는 것일까요?


연결 방법은 몇가지가 있지만, 가장 근본적으로는 뷰모델이 인스턴스가 되어서 뷰의 DataContext에 입력이 되면 됩니다. 일반적으로 뷰의 DataContext는 뷰모델이 들어가 있다고 생각하면 될 것 같습니다.


위의 예제에서 연결한 방법은 다음과 같습니다.


<Page x:Class="T10MVVMSample.Views.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
      xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
      xmlns:controls="using:Template10.Controls"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:local="using:T10MVVMSample.Views"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:vm="using:T10MVVMSample.ViewModels"
      mc:Ignorable="d">


    <Page.DataContext>
        <vm:MainPageViewModel/>
    </Page.DataContext>



디자인 타임이나, 런타임시에 뷰가 인스턴스가 되면, 뷰모델도 인스턴스를 시켜서 DataContext 프로퍼티와 연결해 줍니다.


이 방법 이외에도, 디자인타임 뷰모델과 런타임 뷰모델을 서로 다르게 인스턴스 시켜서 사용하는 방법도 있습니다. 주로 Prism을 이용하거나 Container을 이용해서 객체를 생성하는 경우는 다른 방법을 사용해야 하겠습니다.


* MVVM Pattern 정리


이 예제에서는 MainPage.xaml, MainPageViewModel.cs 2개의 파일을 사용하였으며, Data Binding이라는 기술과, Command를 이용한 사용자 인터렉션 처리를 했습니다. 이렇게 MP를 이용해서 개발을 진행하기 위해서는 추가적으로 개념을 정리하고, 배워야 할 내용들이 많습니다. 아마도 그런 이유 때문에 어렵게 느끼는 것이 아닌가 생각합니다. 하지만, 그렇다고 포기하면 프로젝트를 하는 것은 요원한 일이 됩니다.



6. MVVM Pattern을 사용하기 위해서 기본적인 개념을 이해 하고 있어야 하는 내용들


1) Blend for Visual Studio 2015

: Visual Studio 2015는 코딩용 툴입니다. 디자인 작업은 Blend For Visual Studio 2015를 이용하는 것이 좋습니다.

.2) Binding 기술

: MP에서 가장 중요한 기술로 UWP에서는 OneTime, OneWay, TwoWay 방식을 이용할 수 있습니다.

3) IValueConverter

: Model의 value를 View에 출력할 때 형태를 변경해서 출력하는 것을 도와줍니다.

: Value conversion with IValueConverter 내용을 참고하세요

4) ICommand

: View에서 발생하는 사용자 Interaction을 ViewModel에 전달하는 용도로 사용합니다.

: WPF - ICommand 동작 방식 포스트를 참고하세요

5) Behavior, Action

: Control에 이벤트를 직접적으로 처리를 해야하는 경우에 만들어 사용합니다.

: Creating custom behaviors 내용을 참고하세요

6) Template

: Model의 value를 양식에 맞추어서 ListView, GridView 등의 컨트롤에 연속적으로 출력하기 위해 사용합니다.

: Styling and Templating 내용을 참고하세요.

7) Style

: 컨트롤의 디자인을 변경하거나, 컨트롤에 기능을 추가하는 용도로 만들거나, 수정해서 사용합니다.

: Styling and Templating 내용을 참고하세요.

8) Selector

: DataTemplateSelector, StyleSelector 등 Model의 value값이나 조건에 따라서 다르게 적용해야 하는 경우에 사용합니다.

: DataTemplateSelector class 셀렉터에 대한 정리된 포스트가 잘 없네요..


7. MVVM Pattern이 제대로 적용된 프로젝트가 조금 더 활성되었으면..


위에서 언급했던 S사에서도 사내에서 .Net 개발을 하지 않았기 때문에, WPF도 처음, Prism도 처음, MVVM Pattern도 처음 사용하는 분이 대부분이였습니다. 하지만, 기본 프레임웍을 구축하고 셈플 화면을 만드는 과정에서 꼼꼼하게 주석을 추가하고, 개발 방법에 대한 문서를 정리하고, 약간의 교육만을 진행했는데도 불구하고, 기본 화면 만드는 작업 일정은 무리 없이 진행이 되었습니다.


프로젝트를 진행하는 주체와 프로젝트를 리딩하는 PM, PL이 조금만 MVVM Pattern에 대한 이해와 관심을 가져주고, 프로젝트에 투입되는 인력도 모른다고, 귀찮다고, 이게 내스타일인데~를 외치지 말고 표준 형태의 Pattern을 배우고, 모르면 물어보면서 확인하면서 진행한다면 더욱더 성공하지 않을까 생각됩니다.




Posted by MVP kaki104

댓글을 달아 주세요

  1. 2017.01.25 10:59 행운유수  댓글주소  수정/삭제  댓글쓰기

    좋은 글, 고맙습니다.

  2. 2017.11.27 06:25 지현명  댓글주소  수정/삭제  댓글쓰기

    이벤트 방식에서 메모리 누수가 있다고 했는데 어떻게 메모리 누수가 되나요?

    • 2017.12.21 06:50 신고 MVP kaki104  댓글주소  수정/삭제

      화면이 Loaded될 때 이벤트 핸들러를 연결한 후에 Unloaded가 될 때 이벤트 핸들러를 제거하지 않으면, 해당 화면이 사용되지 않는 페이지라고 인식을 못하는 경우가 발생합니다. 그래서, 화면이 제대로 언로드 않되는 경우에 컨트롤이나 컨트롤과 연결된 데이터들이 지워지지 않아서 메모리닉이 발생하게 됩니다.

  3. 2017.11.30 09:56 김고맙  댓글주소  수정/삭제  댓글쓰기

    좋은글이네요

  4. 2017.12.13 21:24 ladofa  댓글주소  수정/삭제  댓글쓰기

    MVVM에 대해서 인터넷으로 여러 가지 글을 아무리 많이 읽어도 잘 감이 안 오다가 이 글에서 딱 느낌이 왔네요. 고맙습니다.

  5. 2019.11.08 21:20 지나가던나그네  댓글주소  수정/삭제  댓글쓰기

    MVVM 패턴 자체는 컨셉도 좋고 유용하긴 합니다만... wpf에서 mvvm을 구현하려고 하면 불필요한 작업이 너무 많아지는 것 같아요....
    뭔가 좀 하려고하면 바로바로 안되서 우회해야되는 그런 느낌? 물론 아예 안되는건 아니니까 숙달하면 그만이긴 한데... 그래서 그런가 접근성이 떨어져서 국내에 안퍼지는거겠죠?

    • 2019.11.20 22:43 신고 MVP kaki104  댓글주소  수정/삭제

      좋은 의견 감사합니다.

      근본적으로는 MVVM 패턴에 대한 공부를 하려고 해도 할 수 없는 상황이 문제인것 같기도 합니다. 제대로 배울수 있는 곳도 없고, 책도 실무에서 사용할 수 있도록 알려주는 것도 아니고..;;;

윈도우 10 앱 개발(Windows 10 UWP app)


지난 포스트에 그룹을 만들고, 사람을 등록하고, 사람에 사진을 추가한 후에 Training을 시켜서 인식을 할 수 있는 기본 단계까지 작업을 진행 했습니다. 이번 회는 인식작업을 수행하는 과정을 살펴 보도록 하겠습니다.


Face API 사용해보기

Face API 사용해보기 part2

Face API 사용해보기 part3


1. 참고


How to Identify Faces in Image



2. NuGet Package 추가


WriteableBitmapEx 를 추가합니다. 이미지에 빨간 테두리, 퍼런 테두리 등의 작업을 하는데 필요합니다~

자세한 사항은 Face API 사용해 보기 포스트를 보시면 됩니다.



3. IdentifyPage.xaml 추가


사진을 로드하고, 서버에 보내서 탐지 및 인식하는 작업을 수행하는 화면입니다.


<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:FaceIdentify.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:controls="using:Template10.Controls"
    xmlns:ViewModels="using:FaceIdentify.ViewModels"
    x:Class="FaceIdentify.Views.IdentifyPage"
    mc:Ignorable="d">

    <Page.DataContext>
        <ViewModels:IdentifyPageViewModel />
    </Page.DataContext>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="0.7*" />
            <RowDefinition Height="0.3*" />
        </Grid.RowDefinitions>

        <!--페이지 해더-->
        <controls:PageHeader x:Name="pageHeader" Text="Identify Page">
            <!--인식할 사진 추가-->
            <AppBarButton Icon="Pictures" Label="Open Picture" Command="{Binding OpenPictureCommand}" />
        </controls:PageHeader>

        <!--이미지 보기-->
        <Image x:Name="image" Grid.Row="1" Source="{Binding BitmapSource}" />

        <!--인식된 이미지 보기-->
        <ListView Grid.Row="2" ItemsSource="{Binding IdentifyFaceList}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="1*" />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                        <Image Grid.RowSpan="2" Width="40" Height="40"
                               Source="{Binding FaceImage}" Margin="4" />
                        <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Name}"
                                   Style="{StaticResource BodyTextBlockStyle}" />
                        <TextBlock Grid.Row="1" Grid.Column="1" Style="{StaticResource BodyTextBlockStyle}">
                            <Run Text="{Binding Confidence}" />%
                        </TextBlock>
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Page>


디자인 타임에는 아래와 같이 표시 됩니다.




4. IdentifyPageViewModel.cs


using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Windows.Input;
using Windows.ApplicationModel;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.UI;
using Windows.UI.Popups;
using Windows.UI.Xaml.Media.Imaging;
using FaceIdentify.Models;
using FaceIdentify.Services;
using FaceIdentify.Views;
using Template10.Mvvm;

namespace FaceIdentify.ViewModels
{
    /// <summary>
    ///     인식 페이지
    /// </summary>
    public class IdentifyPageViewModel : ViewModelBase
    {
        private BitmapSource _bitmapSource;
        private IList<PersonModel> _identifyFaceList;

        /// <summary>
        ///     기본 생성자
        /// </summary>
        public IdentifyPageViewModel()
        {
            if (DesignMode.DesignModeEnabled)
            {
                //디자인 타임 데이터
            }
            else
            {
                Init();
            }
        }

        /// <summary>
        ///     비트맵 소스
        /// </summary>
        public BitmapSource BitmapSource
        {
            get { return _bitmapSource; }
            set { Set(ref _bitmapSource, value); }
        }

        /// <summary>
        ///     인식한 얼굴 목록
        /// </summary>
        public IList<PersonModel> IdentifyFaceList
        {
            get { return _identifyFaceList; }
            set { Set(ref _identifyFaceList, value); }
        }

        /// <summary>
        ///     사진 열기 커맨드
        /// </summary>
        public ICommand OpenPictureCommand { get; set; }

        /// <summary>
        ///     초기화
        /// </summary>
        private void Init()
        {
            IdentifyFaceList = new ObservableCollection<PersonModel>();
            OpenPictureCommand = new DelegateCommand(OpenPictureCommandHandler);
        }

        /// <summary>
        ///     파일 선택 창 열기
        /// </summary>
        private async void OpenPictureCommandHandler()
        {
            //FileOpePicker 설정
            var openDlg = new FileOpenPicker();
            openDlg.FileTypeFilter.Add(".jpg");
            openDlg.FileTypeFilter.Add(".jpeg");
            openDlg.FileTypeFilter.Add(".png");
            //파일 선택 창 출력
            var file = await openDlg.PickSingleFileAsync();
            if (file == null) return;
            //파일 기본 속성 조회
            var basic = await file.GetBasicPropertiesAsync();
            //4MB 이상이면 메시지 출력
            if (basic.Size > 4*1024*1024)
            {
                var msg = new MessageDialog("Please select a picture below 4MB");
                await msg.ShowAsync();
                return;
            }

            IdentifyFaceList.Clear();

            Busy.SetBusy(true, "Identifying...");

            //처음 화면에 출력용
            var bm = new BitmapImage();
            var wbm = new WriteableBitmap(100, 100);
            //IRandonAccessStream으로 파일을 열고 읽어 들이기
            using (var stream = await file.OpenAsync(FileAccessMode.Read))
            {
                await bm.SetSourceAsync(stream);
                wbm = await BitmapFactory.New(1, 1)
                    .FromStream(stream, BitmapPixelFormat.Bgra8);
            }
            BitmapSource = bm;

            //얼굴인식하고 테두리 표시하기
            //Stream으로 파일을 열어서기
            using (var stream = await file.OpenStreamForReadAsync())
            {
                //이미지에서 얼굴 탐지
                var faces = await FaceAPIHelper.Instance.GetDetectAsync(stream);
                if (faces == null) goto ExitRtn;
                //탐지된 얼굴 표시
                foreach (var face in faces)
                {
                    wbm.DrawRectangle(face.FaceRectangle.Left, face.FaceRectangle.Top,
                        face.FaceRectangle.Left + face.FaceRectangle.Width,
                        face.FaceRectangle.Top + face.FaceRectangle.Height, Colors.Red);
                }

                //한번에 인식 가능한 얼굴의 수10개로 제한
                for (var i = 0; i < faces.Length; i = i + 10)
                {
                    var faceIds = faces.Select(p => p.FaceId)
                        .Skip(i*10)
                        .Take(10)
                        .ToArray();
                    if (!faceIds.Any()) continue;
                    //인식 시작
                    var results = await FaceAPIHelper.Instance.IdentifyAsync(faceIds);
                    //결과를 루프를 돌면서 사용자 정보를 불러 옮
                    foreach (var identifyResult in results)
                    {
                        var identifyPerson = new PersonModel();
                        //탐색된 얼굴 정보 조회
                        var face = faces.First(p => p.FaceId == identifyResult.FaceId);
                        //탐색된 얼굴 이미지(빈 통)
                        var faceImage = new WriteableBitmap(face.FaceRectangle.Width, face.FaceRectangle.Height);
                        //원본 이미지에서 얼굴 부분만 복사
                        faceImage.Blit(new Rect(0, 0, faceImage.PixelWidth, faceImage.PixelHeight)
                            , wbm,
                            new Rect(face.FaceRectangle.Left, face.FaceRectangle.Top, face.FaceRectangle.Width,
                                face.FaceRectangle.Height));
                        //얼굴 추가
                        identifyPerson.AddFace(face.FaceId, faceImage);

                        if (identifyResult.Candidates.Length == 0)
                        {
                            //찾은 사람이 없음
                            identifyPerson.Name = "No one identified";
                        }
                        else
                        {
                            //가장 비슷한 사람 아이디를 반환
                            var candidate = identifyResult.Candidates.First();
                            var candidateId = candidate.PersonId;
                            //사람 정보 조회
                            var person = await FaceAPIHelper.Instance.GetPersonAsync(candidateId);

                            identifyPerson.Name = person.Name;
                            identifyPerson.PersonId = person.PersonId;
                            identifyPerson.Confidence = candidate.Confidence;

                            wbm.DrawRectangle(face.FaceRectangle.Left, face.FaceRectangle.Top,
                                face.FaceRectangle.Left + face.FaceRectangle.Width,
                                face.FaceRectangle.Top + face.FaceRectangle.Height, Colors.Blue);
                        }
                        //목록에 추가
                        IdentifyFaceList.Add(identifyPerson);
                    }
                }
            }
            BitmapSource = wbm;
            ExitRtn:
            Busy.SetBusy(false);
        }
    }
}



5. PersonModel 수정했습니다.


굵은 글씨 부분의 프로퍼티를 추가했습니다.


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using Windows.UI.Xaml.Media.Imaging;
using Microsoft.ProjectOxford.Face.Contract;

namespace FaceIdentify.Models
{
    public class PersonModel : Person, INotifyPropertyChanged
    {
        /// <summary>
        ///     Guid -> Face Guid, BitmapSource -> 사진
        /// </summary>
        public IDictionary<Guid, BitmapSource> Faces = new Dictionary<Guid, BitmapSource>();

        /// <summary>
        ///     바인딩을 하기 위한 프로퍼티
        /// </summary>
        public IList<BitmapSource> FaceImages
        {
            get { return Faces.Select(p => p.Value).ToList(); }
        }

        /// <summary>
        ///     첫번째 사진 무조건 반환, 없으면 말고;;
        /// </summary>
        public BitmapSource FaceImage
        {
            get { return Faces.Values.FirstOrDefault(); }
        }

        /// <summary>
        ///     첫번째 아이디 반환
        /// </summary>
        public string FaceId
        {
            get { return Faces.Keys.FirstOrDefault().ToString(); }
        }

        /// <summary>
        ///     정확도
        /// </summary>
        public double Confidence { get; set; }

        /// <summary>
        ///     얼굴 추가
        /// </summary>
        /// <param name="faceId"></param>
        /// <param name="bs"></param>
        public void AddFace(Guid faceId, BitmapSource bs)
        {
            Faces.Add(faceId, bs);
            //얼굴 추가 메소드를 이용해서 프로퍼티 체인지 이벤트를 강제로 발생시킴
            OnPropertyChanged("FaceImages");
        }

        /// <summary>
        ///     얼굴 삭제
        /// </summary>
        /// <param name="faceId"></param>
        public void RemoveFace(Guid faceId)
        {
            if (!Faces.ContainsKey(faceId)) return;
            Faces.Remove(faceId);
            OnPropertyChanged("FaceImages");
        }

        #region INotifyPropertyChanged, Person 모델을 상속 받았기 때문에 인터페이스를 직접 구현

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion
    }
}



6. Shell.xaml


메인 메뉴에 새로 추가한 페이지로 이동하는 버튼을 추가했습니다.


            <!--  IdentifyPage button  -->
            <Controls:HamburgerButtonInfo PageType="views:IdentifyPage">
                <StackPanel Orientation="Horizontal">
                    <SymbolIcon Width="48" Height="48"
                                Symbol="Contact2" />
                    <TextBlock Margin="12,0,0,0" VerticalAlignment="Center"
                               Text="Identify" />
                </StackPanel>
            </Controls:HamburgerButtonInfo>



7. 실행


실행한 후 새로 추가한 버튼을 눌러서 Identify Page로 이동 했습니다.



사진을 선택하면 자동으로 작업을 수행합니다.



짜잔 최종 결과입니다. 이 사진에서 성소는 파란색 테두리가 그려져있는 얼굴이고 0.6정도의 컨피던스를 가지고 있내요..



사진 인식하기 참 쉽죠? 후후후후 그럼 이 셈플앱 + 아이디어를 추가해서 더 좋은 앱을 만들어 보시면 좋겠습니다.

그리고, 마이크로소프트 코그니티브 서비스는 아이폰, 안드로이드를 모두 지원합니다~


궁금하신 사항은 윈도우 10 앱 개발(Windows 10 UWP app)에 언제나 문의 주세요

그럼 다음에는 또 다른 셈플 앱을 만들어 보도록 하겠습니다.



FaceIdentify_part3.zip


Posted by MVP kaki104

댓글을 달아 주세요

Microsoft MVP 박문찬입니다. 날씨가 추워졌네요..오전에 보일러 고쳐서 참 다행인듯합니다. -0-;;

이전 포스트들을 진행 한 후에 이번 포스트를 보시면 좀 접근하는데 쉬울 것 같습니다.


Face API 사용해보기


Template10을 이용해서 UWP 앱 개발하기 Part1


MVVM Pattern에 대해서 자세히 알고 싶으시면 아래 내용을 참고하시면 됩니다.

Todo list Universal & UWP app


MVVM pattern 설명 동영상(오래전에 녹화한 내용입니다.)

http://youtu.be/f9aQkuoiPz4
http://youtu.be/uGxboAUwciI
http://youtu.be/2lQQiBEjbtU


MVVM Light Toolkit Fundamentals


삽잡이님 블로그

http://shovelman.tistory.com/695
http://shovelman.tistory.com/697
http://shovelman.tistory.com/705
http://shovelman.tistory.com/706



이전 포스트에서는 사진에서 얼굴 위치를 찾아오는 것 까지만 진행 했습니다. 이번에는 사진 속 인물이 누구인지, 학습을 시킨 후에 전혀 다른 사진을 이용해서 해당 사용자를 찾아내는 작업을 해 보도록 하겠습니다.



1. 참고

How to Identify Faces in Image



2. 인증 과정


1) groupId를 이용해서 그룹을 생성합니다. -> CreatePersonGroupAsync

2) 그룹에 사용자를 추가해서 personId를 반환 받습니다. -> CreatePersonAsync

3) groupId와 personId를 이용해서 이미지를 추가합니다.  -> AddPersonFaceAsync

4) 한 사람당 3장 이상의 이미지를 등록 후 머신 러닝을 실행합니다. -> GetPersonGroupTrainingStatusAsync

5) 인식을 원하는 이미지를 이용해서 DetectAsync를 실행합니다.

6) 찾아낸 FaceId를 모아서 인증을 수행합니다. -> IdentifyAsync

7) 인증된 사용자의 정보를 조회 합니다. -> GetPersonAsync


3. FaceIdentify 앱 만들기 시작


File -> New -> Project


Hambuger(Template 10)을 선택하고, FaceIdentify라고 입력하고 OK를 클릭합니다.



Build 후 비주얼 스튜디오를 종료 하고, 다시 시작합니다. (제사한 사항은 Template 10에 대한 포스트 참고)

F5를 눌러서 시작합니다.


햄버거 템플릿의 기본 화면입니다. 여기에 2개의 화면을 추가할 예정입니다. (아무래도 포스트가 2개 이상 필요할 듯하네요;;)





템플릿으로 프로젝트를 만들었는데 실행 했을 때 앱의 기본 골격이 보여지니 내가 필요한 것 몇개만 추가하면 바로 앱이 완성 됩니다. 물론, 솔루션 탐색기에 여러가지 파일들에 대해서 다 알면 좋겠지만, 몰라도 괜찮습니다. 딱 필요한 것만 알고 넘어 가도록 하겠습니다.



4. GroupPage 추가


그룹을 관리하고, 사람을 추가하고, 사진을 추가할 페이지를 추가하도록 하겠습니다.


솔루션 탐색기 Views 폴더에서 마우스 오른쪽 클릭 -> Add -> New Item...을 선택



GroupPage를 입력하고 Add를 클릭합니다.




GroupPage.xaml


<Page
    x:Class="FaceIdentify.Views.GroupPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:FaceIdentify.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:controls="using:Template10.Controls"
    xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
    xmlns:core="using:Microsoft.Xaml.Interactions.Core"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>
       
        <!--페이지 해더-->
        <controls:PageHeader x:Name="pageHeader" Text="Group Page">
            <controls:PageHeader.SecondaryCommands>
                <!--사람 삭제-->
                <AppBarButton Label="Remove Person" Command="{Binding RemovePersonCommand}"/>
            </controls:PageHeader.SecondaryCommands>
            <!--사람 추가-->
            <AppBarButton Icon="AddFriend" Label="Add Person" Command="{Binding AddPersonCommand}"/>
            <!--현재 선택된 사람에 사진 추가-->
            <AppBarButton Icon="Pictures" Label="Add Picture" Command="{Binding AddPictureCommand}"/>
            <!--트레이닝-->
            <AppBarButton Label="Train" Command="{Binding TrainCommand}">
                <AppBarButton.Icon>
                    <FontIcon Glyph="&#xE82E;"/>
                </AppBarButton.Icon>
            </AppBarButton>
        </controls:PageHeader>

        <Grid x:Name="InputName" Grid.Row="1" Margin="0 10 4 0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="Person" VerticalAlignment="Center"/>

            <!--기존 등록된 사람 목록 선택용 콤보 박스-->
            <ComboBox x:Name="comboBox" Grid.Column="1" HorizontalAlignment="Stretch" Margin="10,0,0,0"
                ItemsSource="{Binding Persons}" DisplayMemberPath="Name">
                <interactivity:Interaction.Behaviors>
                    <core:EventTriggerBehavior EventName="SelectionChanged">
                        <core:InvokeCommandAction Command="{Binding SelectionChangedPersonCommand}"
                                                  CommandParameter="{Binding SelectedItem, ElementName=comboBox}"/>
                    </core:EventTriggerBehavior>
                </interactivity:Interaction.Behaviors>
            </ComboBox>

            <!--추가할 사람 이름-->
            <TextBox Grid.Column="2" Margin="6 0 0 0"
                     Text="{Binding PersonName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        </Grid>

        <!--현재 선택된 사람-->
        <Grid x:Name="personName" Grid.Row="2" Margin="0 10 4 0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="Current Person"/>
            <TextBlock Grid.Column="1" Margin="6 0 0 0" Text="{Binding CurrentPerson.Name}"/>
            <TextBlock Grid.Column="2" Text="Reg Pics" Margin="6 0 0 0"/>
            <TextBlock Grid.Column="3" Margin="6 0 0 0"
                       Text="{Binding CurrentPerson.PersistedFaceIds.Length}"
                       HorizontalAlignment="Right"/>
        </Grid>

        <!--현재 선택된 사람에게 추가된 이미지(나중에 다시 보는 기능은 없습니다)-->
        <ListBox Grid.Row="3"
                 Margin="0 10 4 10"
                 ItemsSource="{Binding CurrentPerson.FaceImages}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Image Source="{Binding}" Stretch="UniformToFill"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Page>


XAML 코드 한줄 한줄이 어떻게 만들어 졌는지는..따로 설명을 드리기가 어렵겠네요..ㅜㅜ 지금 진행하고 있는 앱 개발 기초 과정에서 2주간에 걸쳐서 만들어진 코드랍니다.



5. Shell.xaml에 메뉴 추가하기


솔루션 탐색기에서 Shell.xaml 파일을 더블 클릭해서 디자인 화면을 열고, 30라인에 아래 내용을 붙여 넣기 하시면 됩니다. (라인 번호가 보이지 않으면, Tool -> Options -> Text Editor -> All Languages -> Line numbers에 체크를 해주시면 됩니다.)


            <Controls:HamburgerButtonInfo PageType="views:GroupPage">
                <StackPanel Orientation="Horizontal">
                    <SymbolIcon Width="48" Height="48"
                                Symbol="People" />
                    <TextBlock Margin="12,0,0,0" VerticalAlignment="Center"
                               Text="Group" />
                </StackPanel>
            </Controls:HamburgerButtonInfo>


실행해서 디자인 확인하기


다음과 같은 화면이 나오면 정상 입니다. 이제, 내용을 넣도록 하겠습니다.




6. FaceAPIHelper 추가하기

Nuget packages에서 Microsoft.ProjectOxford.Face를 선택해서 추가합니다. (이전 포스트에서 다루었던 내용입니다.)


Services 폴더를 선택하고 마우스 오른쪽, Add -> Class를 선택 -> FaceAPIHelper입력 -> OK


Input your service key에 여러분의 키를 입력하시면 됩니다. 역시 이전 포스트에서 다루는 내용입니다.


using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.ProjectOxford.Face;
using Microsoft.ProjectOxford.Face.Contract;

namespace FaceIdentify.Services
{
    public class FaceAPIHelper
    {
        private static FaceAPIHelper _instance;

        // Create an empty person group
        private readonly string _personGroupId = "myfriends";

        private readonly IFaceServiceClient faceServiceClient =
            new FaceServiceClient("Input your service key");

        private bool _init;

        /// <summary>
        ///     생성자
        /// </summary>
        public FaceAPIHelper()
        {
            Init();
        }

        /// <summary>
        ///     인스턴스
        /// </summary>
        public static FaceAPIHelper Instance
        {
            get { return _instance = _instance ?? new FaceAPIHelper(); }
        }

        /// <summary>
        ///     초기화
        /// </summary>
        public async void Init()
        {
            if (_init) return;

            //사람그룹 목록을 조회한 후에
            var list = await faceServiceClient.ListPersonGroupsAsync();
            //myFriends란 아이디를 가지고 있지 않다면, 사람그룹을 생성한다.
            if (list.All(p => p.PersonGroupId != _personGroupId))
            {
                await faceServiceClient.CreatePersonGroupAsync(_personGroupId, "My Friends");
            }

            _init = true;
        }

        /// <summary>
        ///     사진에서 얼굴 디텍트
        /// </summary>
        /// <param name="fileStream"></param>
        /// <returns></returns>
        public async Task<Face[]> GetDetectAsync(Stream fileStream)
        {
            return await faceServiceClient.DetectAsync(fileStream);
        }

        /// <summary>
        ///     사람 추가
        /// </summary>
        /// <param name="personName"></param>
        /// <returns></returns>
        public async Task<Guid> CreatePersonAsync(string personName)
        {
            var persons = await faceServiceClient.GetPersonsAsync(_personGroupId);
            if (persons.Any(p => p.Name == personName))
            {
                var per = persons.First(p => p.Name == personName);
                return per.PersonId;
            }
            var result = await faceServiceClient.CreatePersonAsync(_personGroupId, personName);
            return result.PersonId;
        }

        /// <summary>
        ///     사람 삭제
        /// </summary>
        /// <param name="personId"></param>
        /// <returns></returns>
        public async Task DeletePersonAsync(Guid personId)
        {
            await faceServiceClient.DeletePersonAsync(_personGroupId, personId);
        }

        /// <summary>
        ///     등록된 사람들 정보 조회
        /// </summary>
        /// <returns></returns>
        public async Task<Person[]> GetPersonsAsync()
        {
            var persons = await faceServiceClient.GetPersonsAsync(_personGroupId);
            return persons;
        }

        /// <summary>
        ///     사람에 얼굴 등록하기
        /// </summary>
        /// <param name="personId"></param>
        /// <param name="fileStream"></param>
        /// <returns></returns>
        public async Task<AddPersistedFaceResult> AddPersonFaceAsync(Guid personId, Stream fileStream)
        {
            try
            {
                // Detect faces in the image and add to Anna
                var result = await faceServiceClient.AddPersonFaceAsync(
                    _personGroupId, personId, fileStream);
                return result;
            }
            catch (FaceAPIException fae)
            {
                Debug.WriteLine(fae.ErrorMessage);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
            return null;
        }

        /// <summary>
        ///     트레이닝 사람그룹
        /// </summary>
        /// <returns></returns>
        public async Task TrainPersonGroupAsync()
        {
            await faceServiceClient.TrainPersonGroupAsync(_personGroupId);

            while (true)
            {
                var trainingStatus = await faceServiceClient.GetPersonGroupTrainingStatusAsync(_personGroupId);
                if (trainingStatus.Status != Status.Running)
                {
                    break;
                }
                await Task.Delay(1000);
            }
        }

        /// <summary>
        ///     인증하기
        /// </summary>
        /// <param name="faceIds"></param>
        /// <returns></returns>
        public async Task<IdentifyResult[]> IdentifyAsync(Guid[] faceIds)
        {
            try
            {
                var result = await faceServiceClient.IdentifyAsync(_personGroupId, faceIds);
                return result;
            }
            catch (FaceAPIException fae)
            {
                Debug.WriteLine(fae.ErrorMessage);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
            return null;
        }

        /// <summary>
        ///     사람 정보 조회
        /// </summary>
        /// <param name="personId"></param>
        /// <returns></returns>
        public async Task<Person> GetPersonAsync(Guid personId)
        {
            var result = await faceServiceClient.GetPersonAsync(_personGroupId, personId);
            return result;
        }
    }
}



7. PersonModel 추가


Models 폴더를 선택하고 마우스 오른쪽, Add -> Class를 선택 -> PersonModel 입력 후 OK


Face API에 있는 Person이라는 클래스를 상속 받아서 몇개의 프로퍼티를 추가하고, 기능을 보강한 모델을 만들어 사용합니다.


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using Windows.UI.Xaml.Media.Imaging;
using Microsoft.ProjectOxford.Face.Contract;

namespace FaceIdentify.Models
{
    public class PersonModel : Person, INotifyPropertyChanged
    {
        /// <summary>
        ///     Guid -> Face Guid, BitmapSource -> 사진
        /// </summary>
        public IDictionary<Guid, BitmapSource> Faces = new Dictionary<Guid, BitmapSource>();

        /// <summary>
        ///     바인딩을 하기 위한 프로퍼티
        /// </summary>
        public IList<BitmapSource> FaceImages
        {
            get { return Faces.Select(p => p.Value).ToList(); }
        }

        /// <summary>
        ///     얼굴 추가
        /// </summary>
        /// <param name="faceId"></param>
        /// <param name="bs"></param>
        public void AddFace(Guid faceId, BitmapSource bs)
        {
            Faces.Add(faceId, bs);
            //얼굴 추가 메소드를 이용해서 프로퍼티 체인지 이벤트를 강제로 발생시킴
            OnPropertyChanged("FaceImages");
        }

        /// <summary>
        ///     얼굴 삭제
        /// </summary>
        /// <param name="faceId"></param>
        public void RemoveFace(Guid faceId)
        {
            if (!Faces.ContainsKey(faceId)) return;
            Faces.Remove(faceId);
            OnPropertyChanged("FaceImages");
        }

        #region INotifyPropertyChanged, Person 모델을 상속 받았기 때문에 인터페이스를 직접 구현

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion
    }
}



8. GroupPageViewModel.cs 추가하기


GroupPage.xaml을 추가했던 비슷한 방법으로, ViewModels 폴더를 선택하고 마우스 오른쪽, Add -> Class를 선택 -> GroupPageViewModel 입력 후 OK



using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Input;
using Windows.ApplicationModel;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.UI.Popups;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Navigation;
using FaceIdentify.Models;
using FaceIdentify.Services;
using FaceIdentify.Views;
using Microsoft.ProjectOxford.Face.Contract;
using Template10.Mvvm;

namespace FaceIdentify.ViewModels
{
    public class GroupPageViewModel : ViewModelBase
    {
        private PersonModel _currentPerson;
        private string _personName;
        private IList<Person> _persons;

        public GroupPageViewModel()
        {
            if (DesignMode.DesignModeEnabled)
            {
                //디자인 타임 데이터
            }
            else
            {
                //런타임 데이터
                Init();
            }
        }

        /// <summary>
        ///     현재 작업 중인(추가한) 사용자
        /// </summary>
        public PersonModel CurrentPerson
        {
            get { return _currentPerson; }
            set { Set(ref _currentPerson, value); }
        }

        /// <summary>
        ///     등록된 사용자 목록
        /// </summary>
        public IList<Person> Persons
        {
            get { return _persons; }
            set { Set(ref _persons, value); }
        }

        /// <summary>
        ///     콤보박스 사용자가 선택되는 경우 실행되는 커맨드
        /// </summary>
        public ICommand SelectionChangedPersonCommand { get; set; }

        /// <summary>
        ///     트레이닝 커맨드
        /// </summary>
        public ICommand TrainCommand { get; set; }

        /// <summary>
        ///     사람 제거 커맨드
        /// </summary>
        public ICommand RemovePersonCommand { get; set; }

        /// <summary>
        ///     사람 추가
        /// </summary>
        public ICommand AddPersonCommand { get; set; }

        /// <summary>
        ///     사진 추가
        /// </summary>
        public ICommand AddPictureCommand { get; set; }

        /// <summary>
        ///     사람 이름
        /// </summary>
        public string PersonName
        {
            get { return _personName; }
            set { Set(ref _personName, value); }
        }

        /// <summary>
        ///     초기화
        /// </summary>
        private void Init()
        {
            //커맨드 생성
            AddPersonCommand = new DelegateCommand(AddPersonCommandHandler,
                () => !string.IsNullOrEmpty(PersonName));

            AddPictureCommand = new DelegateCommand(AddPictureCommandHandler);

            TrainCommand = new DelegateCommand(async () =>
            {
                //화면에 Busy 표시
                Busy.SetBusy(true, "Training....");
                await FaceAPIHelper.Instance.TrainPersonGroupAsync();
                Busy.SetBusy(false);
            });

            SelectionChangedPersonCommand = new DelegateCommand<object>(obj =>
            {
                //obj로 넘어온 데이터를 이용해서 현재 사람 표시
                var person = obj as Person;
                if (person == null) return;
                CurrentPerson = new PersonModel
                {
                    Name = person.Name,
                    PersonId = person.PersonId,
                    PersistedFaceIds = person.PersistedFaceIds
                };
            });

            RemovePersonCommand = new DelegateCommand(async () =>
            {
                if (CurrentPerson == null) return;

                Busy.SetBusy(true, "Removing...");
                await FaceAPIHelper.Instance.DeletePersonAsync(CurrentPerson.PersonId);

                Persons = await FaceAPIHelper.Instance.GetPersonsAsync();
                CurrentPerson = null;
                Busy.SetBusy(false);
            });

            PropertyChanged += (s, e) =>
            {
                switch (e.PropertyName)
                {
                    case "PersonName":
                        //커맨드 사용 가능 여부 확인
                        ((DelegateCommand) AddPersonCommand).RaiseCanExecuteChanged();
                        break;
                }
            };
        }

        private async void AddPictureCommandHandler()
        {
            //FileOpePicker 설정
            var openDlg = new FileOpenPicker();
            openDlg.FileTypeFilter.Add(".jpg");
            openDlg.FileTypeFilter.Add(".jpeg");
            openDlg.FileTypeFilter.Add(".png");
            //파일 선택 창 출력
            var file = await openDlg.PickSingleFileAsync();
            var basic = await file.GetBasicPropertiesAsync();
            //서버에 올릴 수 있는 이미지의 크기는 4MB까지입니다.
            if (basic.Size > 4*1024*1024)
            {
                var msg = new MessageDialog("It is too big image. It can only be up to 4MB.");
                await msg.ShowAsync();
                return;
            }
            //처음 화면에 출력용
            var bm = new BitmapImage();
            using (var stream = await file.OpenAsync(FileAccessMode.Read))
            {
                await bm.SetSourceAsync(stream);
            }
            //얼굴인식하고 테두리 표시하기
            using (var stream = await file.OpenStreamForReadAsync())
            {
                //얼굴 인식에 실패하는 경우 null을 반환합니다.
                var faceResult = await FaceAPIHelper.Instance.AddPersonFaceAsync(CurrentPerson.PersonId, stream);
                if (faceResult == null) return;

                CurrentPerson.AddFace(faceResult.PersistedFaceId, bm);
            }
        }


        /// <summary>
        ///     사람 추가하기
        /// </summary>
        private async void AddPersonCommandHandler()
        {
            if (string.IsNullOrEmpty(PersonName)) return;
            var personId = await FaceAPIHelper.Instance.CreatePersonAsync(PersonName);

            CurrentPerson = new PersonModel
            {
                Name = PersonName,
                PersonId = personId
            };
        }

        /// <summary>
        ///     Template 10의 기능으로 페이지 네비게이션을 통해서 페이지로 진입하면 실행되는 부분입니다.
        /// </summary>
        /// <param name="parameter"></param>
        /// <param name="mode"></param>
        /// <param name="state"></param>
        /// <returns></returns>
        public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode,
            IDictionary<string, object> state)
        {
            Persons = await FaceAPIHelper.Instance.GetPersonsAsync();
        }
    }
}



9. View와 ViewModel 연결


빌드를 한 후 GroupPage.xaml 디자인 화면으로 이동합니다.


네임스페이스를 추가하고 그 아래 Page.DataContrext에 뷰모델을 넣습니다.


참고로 이 방법은 디자인 타임과 런타임에 동일한 방식으로 뷰모델이 생성되는 방식입니다. 뷰모델을 따로 관리하지 않고, 페이지 네비게이션이 발생할 때마다 뷰모델도 생성되었다가 없어지는 구조입니다. 이런 경우 뷰모델 내부에서 저장해야할 데이터는 SessionState를 이용해서 관리합니다.


    xmlns:ViewModels="using:FaceIdentify.ViewModels"

    x:Class="FaceIdentify.Views.GroupPage"
    mc:Ignorable="d">


    <Page.DataContext>
        <ViewModels:GroupPageViewModel/>
    </Page.DataContext>



10. 실행


사람 이름을 입력하면 상단에 사람 추가 버튼이 활성화 됩니다.



정상적으로 추가가 되면 Current Person에 사람 이름이 표시 됩니다.



빨간 색의 사진 추가 버튼을 눌러서 사진을 추가합니다. 이때 사진은 단독 사진으로 하여야 합니다. (여러 사람 얼굴이 나오는 사진도 가능하기는 하지만, 현재 기능 구현이 되어있지 않습니다.)




그래서 저는 총4장을 추가했습니다. 아래는..추가한 사진들 입니다. 흐흐흐





마지막으로 트레이닝 버튼을 눌러서 학습을 시킵니다.



트레이닝은 금방 종료됩니다. ^^;;


그런데, 오늘은 트레이닝까지만... 진행 하겠습니다. 다음에 우주소녀 12명 중에 성소를 찾는 부분을 진행하도록 하겠습니다. -0-;;;; 시간이 늦어서말이죠..


후다닥 =3 =3



11. 소스 (파일이름 틀렸네요;; part2 소스 맞습니다.)


FaceIdentify_part1.zip


Posted by MVP kaki104

댓글을 달아 주세요

UWP 앱을 처음 개발하려고, 비주얼 스튜디오를 설치하고, 새 프로젝트 만들기를 하면 Blank App (Universal Windows) 하나만 보입니다. Hello World 앱 만드는데는 문제가 없지만...진짜 앱을 만들기 위해서는 너무나도 빈약 합니다.


간단한 햄버거 메뉴를 하나 만들려고 해도 해야할일이 참~ 많습니다. 그래서, 이런 문제를 해결하기 위한 방법으로  Template 10이라는 UWP 앱 템플릿을 소개하려고 합니다.



1. Template 10


Microsoft Jerry Nixon이라는 UWP 앱 교육 비디오에 자주 출연하는 멋진 분이 만들어서 공개한 템플릿으로, 앱을 쉽게 만들 수 있도록 Navigation, Suspension, Hamburger control, PageHeader 등의 내용들로 구성 되어 있으며, 이 내용들을 이용한다면 더 쉽게 앱을 만들 수 있습니다.


동영상 강좌를 보면 더 쉽게 이용할 수 있습니다.

Microsoft Virtual Academy Template 10 Training Videos.



2. Template 10에 적용된 규칙 일부 입니다.


1) MVVM 패턴이 기본적으로 적용되어 있습니다. 그래서, Views 폴더에 XAML 파일일 들어가 있습니다.

2) 한개의 뷰에 한개의 뷰모델이 존재합니다.

3) View-model은 ViewModels 폴더에 들어가 있습니다.

4) View-model에서 OnNavigatedTo를 사용할 수 있습니다.

5) Model은 Models 폴더에 들어가 있습니다.

6) NavigationService를 이용해서 navigate를 실행할 수 있습니다.

7) Messenger와 커뮤니케이션 할 수 있습니다.

8) Dependency Injection을 좋아합니다.



3. Template 10에서는 MVVM이 필수 인가요?


MVVM 패턴을 반드시 사용하도록 요구하지는 않지만, 대부분의 XAML app에서 사용하기 때문에 사용하는 것을 권장합니다. 그래서, 다른 MVVM 프레임웍과도 호환이 되도록 만들어져 있습니다.



4. 어떻게 사용할 수 있나요?


Visual Studio 2015를 실행 -> Tools -> Extensions and Updates -> Online -> template 입력 ->

Template 10 Template Pack version : 1.19를 선택 -> Install 클릭


설치하는데 약간 시간이 걸릴 수 있습니다. 완료될 때까지 기다려 주세요~ 이전 버전인 1.18은 템플릿을 만든 후 추가 작업을 해야 했는데..바로 어제 버전이 1.19로 올라가면서 모든 문제가 해결되었습니다.




5. 어떤 템플릿을 만들 수 있나요?


File -> New -> Project를 선택하면, 아래와 같이 3가지 템플릿이 추가된 것을 확인할 수 있습니다.



** 만약 한글 Visual Studio 2015를 사용한다면, 영문으로 Windows라고 되어있는 곳에서 찾아 보아야 합니다.

(1.19에서 수정이 되었는지 여부는 확인을 못했습니다.)




6. Hello Template 10을 만들어 보겠습니다.


Visual Studio 2015 실행 후


File -> New -> Project -> Blank (Template 10) 선택 -> 이름은 HelloT10을 입력하고 OK를 클릭합니다.



아래와 같이 Target Version과 Minimum Version을 선택하는 화면이 나옵니다.

여기서 Windows 10 Anniversary Edition (10.0; Build 14393)이 나오지 않으면, 현재 윈도우 버전이 최신 버전이 아니고, SDK가 설치되어 있지 않기 때문입니다.


포스트를 계속 보시기 위해서는 반드시 최신 윈도우 버전 1607(OS Build 14393.xxx)

Programs and Features에는

Windows Software Development Kit - Windows 10.0.14393.33

Windows Software Development Kit - Windows 10.0.10586.212

이 설치되어있는지 확인하시면 됩니다.


Windows 10 SDK




프로젝트가 만들어지면, F6 키를 눌러서 Build를 한번 꼭 해줍니다.

오른쪽 Solution Explorer에서 References를 확장해서 보았을 때 아래와 같이 나오지 않는다면, Visual Studio를 종료한 후 다시 실행해서 새로 만든 프로젝트를 불러오면 아래와 같이 보일 것입니다.


MainPage.xaml을 더블클릭하면 디자인 화면을 볼 수 있습니다.



디자인 화면이 아래와 같이 나오는지 확인해 주세요~ 화면이 아래와 같이 나오지 않는다면~



Task Manager를 열고, Details 탭을 선택하고, XDesProc.exe를 선택 한 후 End task를 클릭해서, 프로세스를 죽이고 디자인 화면을 다시 열면 잘 보일 것으로 생각됩니다~




MainPage.xaml


<TextBlock .. />을 찾아서 Text 부분을 Hello T10 World를 변경 합니다.


디자인 화면만 보이고, XAML 코드가 보이지 않는다면, 빨간 테두리가 있는 곳에 - 아이콘을 눌러서 화면을 나누어 주시면 코드를 직접 입력할 수 있습니다.




F5를 눌러서 실행 합니다.



이렇게해서 Template 10을 이용해서 UWP 앱을 만드는 과정을 간단하게 살펴 보았습니다.


요즘 계속해서 사용하고 있는데, 상단히 마음에 드는 템플릿입니다. 시간을 내서 후속 포스트를 작성해야하는데..어떻게 될지는 잘 모르겠네요..


좋아요~팍팍 눌러주시면..열심히 작성해 보겠습니다.~~~



Posted by MVP kaki104

댓글을 달아 주세요