'Portable Class Library'에 해당되는 글 2건

개발을 하면서 윈폰이나 윈8이나 앱은 거의 비슷한데 뷰모델을 각각 만들어서 사용해야하는 부분이 매우 귀찮았다. 그래서 처음에 Portable Class Library가 나왔을 때 큰 기대를 했었는데.. 지원하는 Framework에 제약 사항이 많아서 다시 실망하지 않을 수 없었다.

 

어느날 Cross-Platform Development with the .NET Framework라는 게시물을 보고 무언가 방법이 있지 않을까하는 생각을 했었지만, 그것도 잠시.. "이게 뭐야!"라는 생각에 머리를 갸우뚱했었다. 그리고 시간이 지나 윈도우 8.1에 새로운 API들이 속속 등장하고 그 내용 파악을 하는 중에 다시 한번 동일한 내용을 읽게 되었다.

 

"음..음..엇!!"

 

무언가 지금까지 잘못 생각하고 있었구나..라는 생각에 다시 정리를 하게 되었다.

 

결론부터 이야기하면, MVVM pattern으로 개발을 할때 Model, ViewModel은 모두 PCL에 등록해 놓고 사용하는 것이 가능하다는 것이다. 그 이야기를 풀어 나가보자.

 

1. 문서에서 이야기하는 PCL 사용방법

 

1) ExamplePortableLibrary 추상화 클래스를 만든다.
2) ExamplePortableLibrary의 사용은

ExampleLocalSettings.Instance.SetLocalValue("ExampleSetting", "New value to add");

이렇게 한다.

3) SilverlightImplementation : ExampleLocalSettings : Silverlight에 추상화 클래스를 상속받은 클래스를 만든다.
4) AppImplementation : ExampleLocalSettings : Store app에 추상화 클래스를 상속받은 클래스를 만든다.
5) 앱 시작시 인스턴스를 시켜준다.

ExampleLocalSettings.Instance = new SilverlightImplementation(); :

ExampleLocalSettings.Instance = new AppImplementation();

 

그래서 처음에는 이렇게 만들면 동일한 이름을 사용할 수 있으니까.. 각 프로젝트에 뷰모델 만들때 사용할 수 있겠구나..라고 생각했다. (나만 이렇게 생각을 했나?? 흐흐;;)

 

그런데..

 

PCL에 ViewModel을 만들어 놓고 그 뷰 모델안에서

ExampleLocalSettings.Instance.SetLocalValue("ExampleSetting", "New value to add");

메소드를 호출해서 사용한다면? 하나의 뷰 모델을 모든 프로젝트에서 사용할 수 있는 것 아닌가?

 

 

2. 프로젝트 구성

 

PCLSample.PCL : Portable Class Library project (Model and ViewModel include)

PCLSample.SL5 : Silverlight 5 project, Async/AWait package, PortableIoC package

PCLSample.W8 : Windows 8 Store app project, PortableIoC package

PCLSample.Web : MVC4 project,(WebAPI, Silverlight startup)

PCLSample.WP71 : Windows Phone 7.1 Mango project, Async/AWait package, PortableIoC package

PCLSample.WPF : WPF project, PortableIoC package

 

PCL 프로젝트는 크게 2가지 부분으로 나누어진다.

1) HttpClient를 사용하는 방법 :

이전 포스트에서 간단하게 다루어보았기 때문에 이 포스트에서는 다루지 않는다.

2) AbstractFunctions를 사용하는 방법

 

 

3. 전체 프로젝트에서 사용할 ViewModel을 하나 만든다.

MainViewModel.cs

...

        private DelegateCommand clearCommand;
        public DelegateCommand ClearCommand
        {
            get
            {
                if (clearCommand == null)
                {
                    clearCommand = new DelegateCommand(
                        async args =>
                        {
                            var result = await AbstractFunctions.Instance.ConfirmAsync("Are you sure?");
                            if (result == false)
                                return;

                            People.Clear();
                            AbstractFunctions.Instance.MsgBox("Work Complete");
                        });
                }
                return clearCommand;
            }
        }
...

...

        private DelegateCommand fileOpenCommand;
        public DelegateCommand FileOpenCommand
        {
            get
            {
                if (fileOpenCommand == null)
                {
                    fileOpenCommand = new DelegateCommand(
                        async args =>
                        {
                            var exts = string.Empty;
                            var files = await AbstractFunctions.Instance.FileOpenAsync(exts, "ComputerFolder");
                            if (files != null)
                            {
                                foreach (var item in files)
                                {
                                    FileNames.Add(item);
                                }
                            }
                        });
                }
                return fileOpenCommand;
            }
        }
...

3개의 메소드를 사용하는 ViewModel이다. 

 

1) MsgBox : 메시지를 출력하는 메소드

2) ConfirmAsync : 사용자의 확인을 받는 메소드

3) FileOpenAsync : 파일을 열기 대화창을 출력하고 선택된 파일의 경로를 반환 하는 메소드

각 플랫폼마다 비슷한 기능은 가지고 있지만, 네임스페이스라던지 클래스가 전혀 다르다. 하지만 이렇게 사용할 수 있다면 뷰모델을 PCL에 넣어 두고 사용이 가능하다.

 

namespace PCLSample.PCL.Functions
{
    /// <summary>
    /// Main abstract class
    /// </summary>
    public abstract class AbstractFunctions
    {
        /// <summary>
        /// Instance
        /// </summary>
        public static AbstractFunctions Instance { get; set; }

        /// <summary>
        /// MessageBox call method
        /// </summary>
        public abstract void MsgBox(string content);

        /// <summary>
        /// Confirm call method
        /// </summary>
        public abstract System.Threading.Tasks.Task<bool> ConfirmAsync(string context, string title = "Confirm");

        /// <summary>
        /// FileOpen Dialog open method
        /// </summary>
        public abstract System.Threading.Tasks.Task<ICollection<string>> FileOpenAsync(string extensions, string locationId, bool single = true);
    }
}

 

Silverlight

 

    public class SLFunctions : PCL.Functions.AbstractFunctions
    {
        public override void MsgBox(string content)
        {
            MessageBox.Show(content);
        }

        public override async Task<bool> ConfirmAsync(string context, string title = "Confirm")
        {
            MessageBoxResult result = MessageBoxResult.Cancel;
            result = MessageBox.Show(context, title, MessageBoxButton.OKCancel);
            await TaskEx.Delay(1);      //단지 async를 사용하기 위한..
            return result == MessageBoxResult.OK ? true : false;
        }

        public override async Task<System.Collections.Generic.ICollection<string>> FileOpenAsync(string extensions, string locationId, bool single = true)
        {
            // Create an instance of the open file dialog box.
            OpenFileDialog openFileDialog = new OpenFileDialog();

            extensions = "Text Files (.txt)|*.txt|All Files (*.*)|*.*";
            // Set filter options and filter index.
            //openFileDialog.InitialDirectory =
            openFileDialog.Filter = extensions;
            openFileDialog.FilterIndex = 1;
            if (single == true)
                openFileDialog.Multiselect = false;
            else
                openFileDialog.Multiselect = true;

            var results = new List<string>();

            // Call the ShowDialog method to show the dialog box.
            bool? userClickedOK = openFileDialog.ShowDialog();
            // Process input if the user clicked OK.
            if (userClickedOK == true)
            {
                // Open the selected file to read.
                if (single == true)
                {
                    results.Add(openFileDialog.File.FullName);
                }
                else
                {
                    foreach (var item in openFileDialog.Files)
                    {
                        results.Add(item.FullName);
                    }
                }
            }
            await TaskEx.Delay(1);
            return results;
        }

    }

Windows 8 store app

 

    public class W8Functions : PCL.Functions.AbstractFunctions
    {
        static Windows.UI.Popups.MessageDialog msg;
        static bool isShow;

        public override async void MsgBox(string content)
        {
            if (msg == null)
            {
                msg = new Windows.UI.Popups.MessageDialog(content);
            }
            if (isShow == false)
            {
                isShow = true;
                var result = await msg.ShowAsync();
                isShow = false;
            }
        }

        public override async Task<bool> ConfirmAsync(string context, string title = "Confirm")
        {
            MessageDialog msg = new MessageDialog(context, title);
            msg.Commands.Add(new UICommand("OK", _ => { }, "0"));
            msg.Commands.Add(new UICommand("Cancel", _ => { }, "1"));
            msg.DefaultCommandIndex = 1;
            var result = await msg.ShowAsync();
            if (result.Id.ToString() == "0")
            {
                return true;
            }
            return false;
        }

        public override async Task<ICollection<string>> FileOpenAsync(string extensions, string locationId, bool single = true)
        {
            FileOpenPicker picker = new FileOpenPicker();
            picker.SuggestedStartLocation = (PickerLocationId)Enum.Parse(typeof(PickerLocationId), locationId);
            picker.ViewMode = PickerViewMode.List;

            extensions = ".doc|.docx|.log|.msg|.odt|.pages|.rtf|.tex|.txt|.wpd|.wps|.azw|.csv|.dat|.efx|.epub|.gbr|.ged|.ibooks|.key|.keychain|.pps|.ppt|.pptx|.sdf|.tar|.vcf|.xml|.aif|.iff|.m3u|.m4a|.mid|.mp3|.mpa|.ra|.wav|.wma|.3g2|.3gp|.asf|.asx|.avi|.flv|.mov|.mp4|.mpg|.rm|.srt|.swf|.vob|.wmv|.3dm|.3ds|.max|.obj|.bmp|.dds|.dng|.gif|.jpg|.png|.psd|.pspimage|.tga|.thm|.tif|.yuv|.ai|.eps|.ps|.svg|.indd|.pct|.pdf|.xlr|.xls|.xlsx|.accdb|.db|.dbf|.mdb|.pdb|.sql|.apk|.app|.bat|.cgi|.com|.exe|.gadget|.jar|.pif|.vb|.wsf|.dem|.gam|.nes|.rom|.sav|.dwg|.dxf|.gpx|.kml|.asp|.aspx|.cer|.cfm|.csr|.css|.htm|.html|.js|.jsp|.php|.rss|.xhtml|.crx|.plugin|.fnt|.fon|.otf|.ttf|.cab|.cpl|.cur|.dll|.dmp|.drv|.icns|.ico|.lnk|.sys|.cfg|.ini|.prf|.hqx|.mim|.uue|.7z|.cbr|.deb|.gz|.pkg|.rar|.rpm|.sit|.sitx|.gz|.zip|.zipx|.bin|.cue|.dmg|.iso|.mdf|.toast|.vcd|.c|.class|.cpp|.cs|.dtd|.fla|.h|.java|.lua|.m|.pl|.py|.sh|.sln|.vcxproj|.xcodeproj|.bak|.tmp|.crdownload|.ics|.msi|.part|.torrent";
            var ary = extensions.Split('|');
            foreach (string extension in ary)
            {
                picker.FileTypeFilter.Add(extension);
            }

            var results = new List<string>();
            if (single == true)
            {
                var file = await picker.PickSingleFileAsync();
                results.Add(file.Path);
            }
            else
            {
                var files = await picker.PickMultipleFilesAsync();
                foreach (var item in files)
                {
                    results.Add(item.Path);
                }
            }
            return results;
        }

    }

 

 

Windows Phone 7.1

 

    public class WP71Functions : PCL.Functions.AbstractFunctions
    {
        public override void MsgBox(string content)
        {
            MessageBox.Show(content);
        }

        public override async Task<bool> ConfirmAsync(string context, string title = "Confirm")
        {
            MessageBoxResult result = MessageBoxResult.Cancel;
            result = MessageBox.Show(context, title, MessageBoxButton.OKCancel);
            await TaskEx.Delay(1);      //단지 async를 사용하기 위한..
            return result == MessageBoxResult.OK ? true : false;
        }

        public override async Task<ICollection<string>> FileOpenAsync(string extensions, string locationId, bool single = true)
        {
            var result = new List<string>();
            MsgBox("Not Support this method");
            await TaskEx.Delay(1);
            return result;
        }
    }

 

WPF

 

    public class WPFFunctions : PCL.Functions.AbstractFunctions
    {
        public override void MsgBox(string content)
        {
            MessageBox.Show(content);
        }

        public override async Task<bool> ConfirmAsync(string context, string title = "Confirm")
        {
            MessageBoxResult result = MessageBoxResult.Cancel;
            result = MessageBox.Show(context, title, MessageBoxButton.OKCancel);
            await Task.Delay(1);    //단지 async를 사용하기 위한..
            return result == MessageBoxResult.OK ? true : false;
        }

        public override async Task<ICollection<string>> FileOpenAsync(string extensions, string locationId, bool single = true)
        {
            // Create an instance of the open file dialog box.
            OpenFileDialog openFileDialog = new OpenFileDialog();

            // Set filter options and filter index.
            openFileDialog.Filter = extensions;
            openFileDialog.FilterIndex = 1;
            if (single == true)
                openFileDialog.Multiselect = false;
            else
                openFileDialog.Multiselect = true;

            var results = new List<string>();

            // Call the ShowDialog method to show the dialog box.
            bool? userClickedOK = openFileDialog.ShowDialog();
            // Process input if the user clicked OK.
            if (userClickedOK == true)
            {
                // Open the selected file to read.
                if (single == true)
                {
                    results.Add(openFileDialog.FileName);
                }
                else
                {
                    foreach (var item in openFileDialog.FileNames)
                    {
                        results.Add(item);
                    }
                }
            }
            await Task.Delay(1);
            return results;
        }
    }

 

3. MainViewModel을 사용하기 위한 PortableIoC 설정

 

        #region PortableIOC, Document http://portableioc.codeplex.com/documentation
        private static IPortableIoC container;

        public static TService GetInstance<TService>(string label = "") where TService : class
        {
            return container.Resolve<TService>(label);
        }
        #endregion

        //각 플랫폼별로 이부분만 약간씩 다르다. 자세한 사항은 프로젝트 소스를 참고 한다.

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            //PortableIOC
            Bootstrap();
            this.RootVisual = new MainPage();
        }

        //PortableIOC
        private static void Bootstrap()
        {
            // Create the container as usual.
            var container = new PortableIoc();

            // Register your types, for instance:
            container.Register<IMainViewModel>(ioc => new MainViewModel());

            // Store the container for use by the application.
            App.container = container;

            // Support UI cross thread
            StaticFunctions.BaseContext = System.Threading.SynchronizationContext.Current;

            // Create the AbstractFunctions instance
            PCLSample.PCL.Functions.AbstractFunctions.Instance = new SLFunctions();
        }

4. View에 MainViewModel 연결

    //각 플랫폼별 약간의 차이가 있다. 자세한 사항은 소스를 참고 한다.

    public partial class MainPage : UserControl
    {
        public IMainViewModel ViewModel
        {
            get { return DataContext as IMainViewModel; }
            set { DataContext = value; }
        }

        public MainPage()
        {
            InitializeComponent();

            ViewModel = App.GetInstance<IMainViewModel>();
        }
    }

5. 실행 결과

MainViewModel을 Silverlight, Windows 8 store app, Windows Phone 7.1, WPF에 적용한 화면

 

6. 끝으로

실버라이트의 경우 로컬 파일의 경로를 알기 위해서는 레지스트리에 AllowElevatedTrustAppsInBrowser를 추가해 주어야 한다고 한다. 실버라이트 프로젝트의 silverlight_InBrowserSetting.reg 파일을 더블클릭하면 레지스트리에 값이 추가된다.

 

PCL을 이용한 멀티 플랫폼용 Infrastructure를 만들려고 계획 중이다. 기본적으로는 오픈 소스 형태로 제공되며, 각각의 플랫폼에서 모두 사용이 가능한 메소드를 추가할 수 있는 분들의 지원을 기다린다.(Xaml, MVVM pattern에 대한 기본적인 내용을 알고 있는 분)

kaki104@nate.com

 

7. 소스

http://sdrv.ms/1au2D3W

 

블로그 이미지

kaki104

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

Portable Class Library에서 HttpClient를 사용하는 방법에 대해 간단하게 소개하려고 한다.

 

1. Portable Class Library Change Target Frameworks

* .Net Framework 4.5 (4.0도 가능할듯..)

* Silverlight 4 and higher

* Windows Phone 7.5 and higher

* .Net for Windows Store apps

 

2. 필요한 Nuget Package

 

* Base Class Library (BCL) Blog : 여기가 메인 이군요

http://blogs.msdn.com/b/bclteam/

 

* Microsoft HTTP Client Libraries

PCL에서 HttpClient를 사용하도록 해주는 패키지

 

* Async for .Net Framework 4, Silverlight 4 and 5, and Win...

PCL에서 Async, AWait를 사용하도록 해주는 패키지

이 라이브러리를 설치하면 Microsoft BCL Build Components, BCL Portablity Pack for .NET Framework 4, Silverlight 4...라는 라이브러리가 동시에 설치됨

http://blogs.msdn.com/b/bclteam/archive/2012/10/22/using-async-await-without-net-framework-4-5.aspx

 

* Json.NET

Json 데이터 처리를 위한 패키지

 

* WebUtility for Portable Class Library

WebUtility (HttpUtility) in Portable Class Library

PCL에서 HtmlDecode, HtmlEncode, UrlDecode, UrlEncode를 해주는 패키지

string decodedInput = WebUtility.HtmlDecode(encodedInput);
string encodedInput = WebUtility.HtmlEncode(Input);
string decodedUrl = WebUtility.UrlDecode(encodedUrl);
string encodedUrl = WebUtility.UrlEncode(url);

 

 

 

3. WebAPIBase.cs

 

    public abstract class WebAPIBase<T> : BindableBase
    {
        /// <summary>
        /// 베이스Uri
        /// </summary>
        private string baseUri;

        /// <summary>
        /// 서비스 Uri
        /// </summary>
        public string ServiceUri { get; set; }

        private ICollection<T> resultCollection;

        /// <summary>
        /// 결과 컬렉션
        /// </summary>
        public ICollection<T> ResultCollection
        {
            get { return resultCollection; }
            set
            {
                resultCollection = value;
                OnPropertyChanged("ResultCollection");
            }
        }

        /// <summary>
        /// 결과코드
        /// </summary>
        public string ResultCode { get; set; }

        /// <summary>
        /// 결과메시지
        /// </summary>
        public string ResultMsg { get; set; }

        /// <summary>
        /// 반환갯수
        /// </summary>
        public int NumOfRows { get; set; }

        private int pageNo;
        /// <summary>
        /// 페이지 번호
        /// </summary>
        public int PageNo
        {
            get { return pageNo; }
            set
            {
                pageNo = value;
                OnPropertyChanged("PageNo");
            }
        }

        private int totalCount;
        /// <summary>
        /// 전체 카운트
        /// </summary>
        public int TotalCount
        {
            get { return totalCount; }
            set
            {
                totalCount = value;
                OnPropertyChanged("TotalCount");
            }
        }

        /// <summary>
        /// 토탈 페이지 = totalCount / NumOfRows + 1
        /// </summary>
        public int TotalPage { get; set; }

        /// <summary>
        /// 생성자
        /// </summary>
        public WebAPIBase()
        {
            ResultCollection = new ObservableCollection<T>();
            baseUri = "http://localhost:2852";
            ServiceUri = "";
            NumOfRows = 30;
            PageNo = 1;

            this.PropertyChanged +=
                (s, e) =>
                {
                    switch (e.PropertyName)
                    {
                        case "TotalCount":
                            if (TotalCount > 0)
                            {
                                TotalPage = TotalCount / NumOfRows;
                                if ((TotalCount % NumOfRows) > 0)
                                    TotalPage++;
                            }
                            else
                            {
                                TotalPage = 0;
                            }
                            break;
                    }
                };
        }

        /// <summary>
        /// 조회
        /// </summary>
        protected async Task<string> GetData(string para)
        {
            string result = string.Empty;
            using (HttpClient hc = new HttpClient())
            {
                try
                {
                    var uri = string.Format("{0}{1}", baseUri, ServiceUri);
                    result = await hc.GetStringAsync(uri);
                }
                catch (Exception ex)
                {
                    System.Diagnostics.Debug.WriteLine(ex.Message);
                }
            }
            return result;
        }

        /// <summary>
        /// 조회
        /// </summary>
        public abstract Task<bool> Get(string para);

    }

 

4. TestWebAPI.cs

 

    public class TestWebAPI : WebAPIBase<PersonModel>
    {
        /// <summary>
        /// 인스턴스
        /// </summary>
        private readonly static TestWebAPI instance = new TestWebAPI();

        /// <summary>
        /// 인스턴스 프로퍼티
        /// </summary>
        public static TestWebAPI Instance
        {
            get
            {
                return instance;
            }
        }
       
        /// <summary>
        /// 생성자
        /// </summary>
        public TestWebAPI()
            : base()
        {
            ServiceUri = "/api/values";
        }

        /// <summary>
        /// WebAPI 호출
        /// </summary>
        /// <param name="para"></param>
        /// <returns></returns>
        public override async System.Threading.Tasks.Task<bool> Get(string para)
        {
            bool resultValue = false;

            var result = await GetData(para);
            if (!string.IsNullOrEmpty(result))
            {
                ResultCollection = Newtonsoft.Json.JsonConvert.DeserializeObject<List<PersonModel>>(result);
                TotalCount = ResultCollection.Count;
                resultValue = true;
            }
            return resultValue;
        }

    }

 

5. MainViewModel.cs

 

    public class MainViewModel : BindableBase, IMainViewModel
    {
        public ObservableCollection<PersonModel> People { get; set; }

        public MainViewModel()
        {
            People = new ObservableCollection<PersonModel>();

            People.Add(new PersonModel { Id = 1, Name = "kaki104", Age = 11, Sex = true });
        }

        private async void GetPeople()
        {
            var result = await TestWebAPI.Instance.Get("");
            if (result == true)
            {
                foreach (var item in TestWebAPI.Instance.ResultCollection)
                {
                    People.Add(item);
                }
            }
        }

        private string _text;

        public string Text
        {
            get { return _text; }
            set
            {
                _text = value;
                OnPropertyChanged("Text");
            }
        }

        private PersonModel person;

        public PersonModel Person
        {
            get { return person; }
            set
            {
                person = value;
                OnPropertyChanged("Person");
            }
        }

        private DelegateCommand getListCommand;
        public DelegateCommand GetListCommand
        {
            get
            {
                if (getListCommand == null)
                {
                    getListCommand = new DelegateCommand(
                        args =>
                        {
                            GetPeople();
                        });
                }
                return getListCommand;
            }
        }

        private DelegateCommand clearCommand;
        public DelegateCommand ClearCommand
        {
            get
            {
                if (clearCommand == null)
                {
                    clearCommand = new DelegateCommand(
                        args =>
                        {
                            People.Clear();
                        });
                }
                return clearCommand;
            }
        }
       

        private DelegateCommand personClickedCommand;
        public DelegateCommand PersonClickedCommand
        {
            get
            {
                if (personClickedCommand == null)
                {
                    personClickedCommand = new DelegateCommand(
                        args =>
                        {
                        });
                }
                return personClickedCommand;
            }
        }
    }

 

6. 결과

Silverlight 5와 Windows Phone 7.1에서 사용한 화면

현재 ViewModel까지 PCL에 올려놓고 WPF, Silverlight, Windows Phone, Windows 8 store app까지 함께 사용하는 방법에 대해 테스트 하는 중이라 전체 소스를 공개 하지는 않는다.

전에 facebook에서 HttpClient를 사용하지 못해서 안타까워하는 분이 있는 것 같아서 포스트 한다.

 

 

블로그 이미지

kaki104

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

티스토리 툴바