|
개발을 하면서 윈폰이나 윈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에 대한 기본적인 내용을 알고 있는 분)
7. 소스
첫댓글 머리가 빙빙 돕니다...
흐흐 너무 어려워하지 마세용..핵심만 가지고 가시면 됩니다. 1개의 뷰모델로 모든 플랫폼에서 사용한다라는..