잊지 않겠습니다.

1. 속도가 빨라졌다. 

전에 VS2010이 뭐같은 녀석이였는지, VS2010에서 시작되는 화면을 보다보면 속이 터져버릴것만 같았다. 

VS2012의 경우에는 속도 자체가 빨라진것보다 Solution을 Lazy Loading을 시켜서 좀 더 빨라보이는 트릭을 사용한다. 

물론 트릭뿐 아니라, 실질적인 속도 자체도 더 빨라졌다. UI의 반응 속도라던지, compile 시의 속도가 향상된 것이 눈에 보인다. 

(물론 측정한것은 아니다. 체감으로.. )


2. 코드의 가독성이 높아졌다.

VS2010의 화려한 인터페이스에서 VS2012의 조금은 투박한 인터페이스를 보면 이건 퇴보가 아닌가. 하는 생각이 들지만, 이 상태로 3~4 시간 코딩을 해보면 확실히 느낌이 다르다. 전에 보이던 인터페이스의 화려한 색이 은근히 가독성을 떨어트리고 있었던 것이 아닌가 하는 생각이 들 정도로 UI 자체에서의 코드에 대한 집중도를 높여준다. 


3. Solution Explorer가 향상되었다.

기존에는 Pending Explorer, Solution Explorer 등의 여러 파일 탐색기에서 각각의 다른 기능을 수행하고 있다. 그런데 거의 모든 기능이 Solution Explorer에 통합이 되었다. 간단한 Toggle 동작만으로 SVN에 올려야지 되는 파일리스트 및 변경사항을 알아내는데 도움을 준다. 

또한, Solution Explorer에서 Class의 method tree를 열수 있는 기능이 추가 되어서, 파일을 하나하나 열지 않고 객체의 method를 직접 찾아가는데 도움을 준다. 


4. Console Application과 Class Library의 default platform이 "Any CPU"로 변경되었다.

개인적으로는 이 점 하나만으로도 VS2010을 버리고, VS2012로 갈아타야지 된다고 생각한다. VS2010은 default platform이 "x86"이기 때문에 개발후 배포작업에 있어서 Reference Linking 작업을 실수를 하거나, x64 system에서 개발하는 개발자와 x86 system에서 개발하는 개발자간의 suo 파일이 달라져서 전체적인 시스템이 달라진것과 다름없는 형태를 보이고 있었다. 특히 svn이나 git에 올리지 않는 suo 파일에 설정된 platform 정보가 딸려다니기 때문에 타개발자와의 소스 호환에도 어려움이 있고, 이 부분이 꼬이면 한 30여개가 넘는 project들 중에서 어떤 msil이 32bit로 따로 설정된것인지 찾는 귀찮고 초보 개발자들은 매우 당황하는 일들을 원천적으로 막을 수 있다. (무조건 "Any CPU"로 개발하는거다!)


5. solution 파일과 proj 파일이 VS2010과 공유된다.

이건 MS가 VS2010 SP1에서부터 준비한 결과인것 같은데, solution과 proj 파일이 VS2010과 VS2012간에는 차이가 없게 열리거나, VS2012에서 migration된 proj 파일이 VS2010에서 아무런 문제 없이 사용이 가능하다. 단 NuGet 1.X 를 이용한 package가 있는 경우에는 VS2012에서 에러가 발생한다. NuGet을 이용하지 않은 project라면 당장 VS2012로 사용해도 무방하다. 


간만에 MS가 Tool을 잘만들어서 내보낸것 같다. 오늘 하루종일 VS2012와 Windows 8으로 삽질을 하고 있었는데... Windows 8에 대해서는 좀 더 사용해보고 글을 하나 적어볼만 한 내용들이 좀 있는것 같고. 


PS. MSSQL이 분리가 되었다. 이건 굉장히 바람직한것이 아닐까 싶다. 몇몇 개발자들은 자신의 컴에 MSSQL Express가 설치가 자동으로 되어있는지도 모르는 사람들이 꽤 있기도 했고. 


PS2. MSSQL2012도 매우 향상된 느낌이 든다. 속도 뿐 아니라, 시스템에서의 안정성 자체도 좀 나아진것 같아서 좀더 가지고 놀면서 테스트를 해봐야지 될 것 같다.

Posted by xyzlast Y2K

MVC4에서 API Controller가 나왔지만, 기존 MVC3에서도 API Controller와 거의 유사하게 동작이 가능한 API Controller를 작성 가능하다.


API Controller와 동일하게 동작하기 위해서는 Json형태로만 return을 시켜주면 되기 때문에, 굳이 기존의 APIController를 사용할 필요성은 없어보이기도 한다. 다만 XmlRpc와 같은 다른 Protocol을 충족시키기위해서는 필요한 사항이 있더라도 말이다. 


또한, API서버를 구축하고, 구축된 서버를 이용한 Cross Domain 문제를 해결하기 위해서는 Response의 Header에 AcessControlAllowOrgin 항목을 을 추가시켜줘야지 된다. 이는 이 API 서버에서 사용이 허용된 사이트가 어디인지를 확인하는 것으로 반드시 들어가줘야지 된다. 이 항목을 넣어주는 방법으로는 다음 3가지 방법이 존재한다. 



1. IIS에 기본 설정을 넣어주는 방법

> Response Header 설정을 이용. 모든 response에 Access-Control-Allow-Orgin을 허용으로 넣어준다. 



2. Global의 Application_BeginRequest을 이용하는 방법

> Global.asax.cs파일에 Applicaiton_BeginRequest를 구현한다.


        protected void Application_BeginRequest(object sender, EventArgs e)
        {
            HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.NoCache);
            HttpContext.Current.Response.Cache.SetNoStore();
            SetEnabledCrossDmainAjaxCall();
        }

        private void SetEnabledCrossDmainAjaxCall()
        {
            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*");
            if(HttpContext.Current.Request.HttpMethod == "OPTIONS")
            {
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST");
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
                HttpContext.Current.Response.AddHeader("Access-Control-Max-Age", "1728000");
                HttpContext.Current.Response.End();
            }
        }


3. Action Filter의 OnResultExcuted를 구현, ActionFilter를 적용하는 방법

> ActionFilter를 구현한다.


    public class CrossDomainAjaxCallAttribute : ActionFilterAttribute
    {
        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            filterContext.HttpContext.Response.AddHeader("Access-Control-Allow-Origin", "*");
            base.OnResultExecuted(filterContext);
        }
    }


Posted by xyzlast Y2K
개인적 관심사로 NoSQL중 RDBMS와 가장 유사하기도 하고, 중대용량에 적합하다고 하는 mongodb를 좀 건드려봤다. 
http://mongodb.org 에서 BSON driver를 다운 받고, driver를 이용하면 매우 쉽게 mongodb를 handling할 수 있다.


간단한 test code들은 다음과 같다.

 
[TestFixture]
public class MongoDbTest
{
    [Test]
    public void CreateSchema()
    {
        string connectionString = "mongodb://localhost";
        //서버 접속
        MongoServer server = MongoServer.Create(connectionString);

        //Database 이름 설정
        var dbSetting = server.CreateDatabaseSettings("mongoDbTest2");
        dbSetting.Credentials = new MongoCredentials("ykyoon", "newPassword");
        var db = server.GetDatabase(dbSetting);

        Assert.That(db != null);

        //Create Table
        db.CreateCollection("NewCollection");
        var collection = db.GetCollection("NewCollection");
    }

    [Test]
    public void InsertTest()
    {
        string connectionString = "mongodb://localhost";
        MongoServer server = MongoServer.Create(connectionString);

        var data01 = new BsonDocument();
        data01.Add("author", "ykyoon");
        data01.Add("name", "first bsonDocument");

        //var dbSetting = server.CreateDatabaseSettings("mongoDbTest");
        //var dbNames = server.GetDatabaseNames();

        var db = server.GetDatabase("mongoDbTest");
        //db.CreateCollection("Users");

        var userCollection = db.GetCollection("Users");
        userCollection.Insert(data01);

        Assert.That(server.DatabaseExists("mongoDbTest"));
    }

    [Test]
    public void SelectTest()
    {
        var connectionString = "mongodb://localhost";
        MongoServer server = MongoServer.Create(connectionString);
        var db = server.GetDatabase("mongoDbTest");

        var userCollection = db.GetCollection("Users");
        var qq = Query.EQ("author", "ykyoon");

        var users = userCollection.Find(qq);
        foreach(var user in users)
        {
            Console.WriteLine(user);
        }
    }
}
Posted by xyzlast Y2K
Network Copy등을 하기 위해서 ID와 Password를 알고 있는데, code 상으로 구현하는 방법을 한번 정리.

일단, WindowsIdentify는 win32 dll을 이용하고, Windows Token을 사용한다. Windows Token은 어떤 시스템에 Logon한 이후에, 일정시간 유지가 되는 Windows의 memory안에 자신의 Identity를 저장하게 된다. 

먼저, Win32의 [DllImport("advapi32.dll", SetLastError = true)] 를 이용해서 LogonUser method를 dll import 시켜온다.

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);


마지막 값인 phToken값을 이용해서 WindowsIdentify를 얻어 내면 타 시스템에 접근이 가능하다. 여기에서 userName은 upn형태로 넣어져야지 되며, 이 형식은 ykyoon@vplex.net 과 같은 email과 같은 형식으로 입력이 되어야지 된다.

다음은 NetworkCopy를 하는 Test code이다. 




[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
[DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private unsafe static extern int FormatMessage(int dwFlags, ref IntPtr lpSource, int dwMessageId, int dwLanguageId, ref String lpBuffer, int nSize, IntPtr* Arguments);

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public extern static bool CloseHandle(IntPtr handle);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public extern static bool DuplicateToken(IntPtr ExistingTokenHandle, int SECURITY_IMPERSONATION_LEVEL, ref IntPtr DuplicateTokenHandle);

public unsafe static string GetErrorMessage(int errorCode)
{
    int FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100;
    int FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
    int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;

    int messageSize = 255;
    String lpMsgBuf = "";
    int dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;

    IntPtr ptrlpSource = IntPtr.Zero;
    IntPtr prtArguments = IntPtr.Zero;

    int retVal = FormatMessage(dwFlags, ref ptrlpSource, errorCode, 0, ref lpMsgBuf, messageSize, &prtArguments);
    if(0 == retVal)
    {
        throw new Exception("Failed to format message for error code " + errorCode + ". ");
    }

    return lpMsgBuf;
}

static void Main(string[] args)
{
    const int LOGON32_PROVIDER_DEFAULT = 0;
    const int LOGON32_LOGON_INTERACTIVE = 2;
    const int SecurityImpersonation = 2;

    var tokenHandle = IntPtr.Zero;
    bool returnValue = LogonUser(@"Administrator@vplex.net", "network ip in here", "this is password", 9, 0, ref tokenHandle);
    Console.WriteLine("LogonUser called.");
    if(false == returnValue)
    {
        int ret = Marshal.GetLastWin32Error();
        Console.WriteLine("LogonUser failed with error code : {0}", ret);
        Console.WriteLine("\nError: [{0}] {1}\n", ret, GetErrorMessage(ret));
        int errorCode = 0x5; //ERROR_ACCESS_DENIED
        throw new System.ComponentModel.Win32Exception(errorCode);
    }
    else
    {
        var ykyoonAdmin = new WindowsIdentity(tokenHandle);
        var wic = ykyoonAdmin.Impersonate();
        File.Copy("C:\\System.Web.Mvc.dll", "\\\\10.30.3.24\\c$\\abc.dll", true);
        wic.Undo();
        wic.Dispose();
    }
    CloseHandle(tokenHandle);
}
Posted by xyzlast Y2K
PropertyInfo를 이용해서 간단히 슥삭. 너무나 편하게 debuging할 수 있는 방법이라서 자주 사용하기도 하고, 긁어서 그냥 넣어주면 편해서 올려둔다. 기본적으로 Property Invoke를 이용해서 각 값을 얻어내고, 그 값을 표시하는 방법인데... 이모저모로 편하구. 



public override string ToString()
{
    StringBuilder sb = new StringBuilder();
    sb.AppendLine(string.Format("<{0}>", GetType().Name));
    foreach(PropertyInfo property in GetType().GetProperties())
    {
        string propertyName = property.Name;
        object value = property.GetValue(this, null);
        if(value != null)
        {
            if(value is IEnumerable && !(value is string))
            {
                sb.AppendLine(string.Format("<{0}>", propertyName));
                IEnumerator enumerator = ((IEnumerable)value).GetEnumerator();
                while(enumerator.MoveNext())
                {
                    sb.AppendLine(enumerator.Current.ToString());
                }
                sb.AppendLine(string.Format("", propertyName));
            }
            else
            {
                sb.AppendLine(string.Format("<{0}>{1}", propertyName, value.ToString()));
            }
        }
    }
    sb.Append(string.Format("", GetType().Name));
    return sb.ToString();
}
Posted by xyzlast Y2K
WMI query를 이용해서 remote computer에 대한 system 작업을 할 때, .net으로 하는 방법 간단 예제.


try
{
    ConnectionOptions connectionOptions = new ConnectionOptions()
                                                {
                                                    Impersonation = ImpersonationLevel.Impersonate,
                                                    Username =
                                                        string.Format("{0}\\{1}", txDomain.Text,
                                                                    txUserName.Text),
                                                    Password = txPassword.Text,
                                                    Authentication = AuthenticationLevel.Default,
                                                    EnablePrivileges = true
                                                };

    ManagementScope scope = new ManagementScope(txNamespace.Text, connectionOptions);
    ObjectQuery query = new ObjectQuery(txQuery.Text);
    ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query);
    ManagementObjectCollection managementObjects = searcher.Get();

    txResult.Text = string.Empty;

    StringBuilder sb = new StringBuilder();
    foreach (var o in managementObjects)
    {
        System.Diagnostics.Debug.WriteLine(o.ToString());
        sb.AppendLine(o.ToString());
    }
    txResult.Text = sb.ToString();
    managementObjects.Dispose();
    searcher.Dispose();
}
catch(Exception ex)
{
    txResult.Text = string.Empty;
    txResult.Text = ex.Message + Environment.NewLine + ex.StackTrace;
}
Posted by xyzlast Y2K

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="specialize">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State (http://schemas.microsoft.com/WMIConfig/2002/State) " xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance (http://www.w3.org/2001/XMLSchema-instance) ">
            <ProductKey>6HFGM-3KFJB-HFQJW-793WD-GKQHY</ProductKey>
        </component>
    </settings>
    <settings pass="generalize">
        <component name="Microsoft-Windows-Security-Licensing-SLC" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State (http://schemas.microsoft.com/WMIConfig/2002/State) " xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance (http://www.w3.org/2001/XMLSchema-instance) ">
            <SkipRearm>1</SkipRearm>
        </component>
    </settings>
</unattend>
Posted by xyzlast Y2K
Controller 객체를 사용해서 각 method를 테스트하는 방법은 매우 뛰어난 테스팅 방법이지만, 불행하게도 Session 및 Identify에 대한 내용을 같이 테스팅 하는 것은 불가능하다. 특히, Session에 Wizard Data 또는 사용자에 따른 개인 정보를 저장하고 있는 경우에는 Controller 객체만을 사용하는 것은 불가능하다.

그래서, Mock을 이용해서 HttpContext를 구성하고, 구성된 HttpContext를 ControllerContext로 구성하여 Controller를 사용하게 되면 위와 같은 문제를 모두 해결 할 수 있다.


public class FakeSession : HttpSessionStateBase
{
    private readonly SessionStateItemCollection _sessionItems;
    public FakeSession(SessionStateItemCollection sessionItems)
    {
        _sessionItems = sessionItems;
    }

    public override void Add(string name, object value)
    {
        _sessionItems[name] = value;
    }

    public override int Count
    {
        get
        {
            return _sessionItems.Count;
        }
    }

    public override IEnumerator GetEnumerator()
    {
        return _sessionItems.GetEnumerator();
    }

    public override NameObjectCollectionBase.KeysCollection Keys
    {
        get
        {
            return _sessionItems.Keys;
        }
    }

    public override object this[string name]
    {
        get
        {
            return _sessionItems[name];
        }
        set
        {
            _sessionItems[name] = value;
        }
    }

    public override object this[int index]
    {
        get
        {
            return _sessionItems[index];
        }
        set
        {
            _sessionItems[index] = value;
        }
    }
    public override void Remove(string name)
    {
        _sessionItems.Remove(name);
        }
}

protected T GetContextedController(T controller, string userName, FakeSession sessionState) where T : ControllerBase
{
    //Register Route
    RouteCollection routes = new RouteCollection();
    MvcApplication.RegisterRoutes(routes);

    //Build Mock HttpContext, Request, Response
    var mockHttpContext = new Moq.Mock();
    var mockRequest = new Moq.Mock();
    var mockResponse = new Moq.Mock();

    //Setup Mock HttpContext 
    mockHttpContext.Setup(x => x.Request).Returns(mockRequest.Object);
    mockHttpContext.Setup(x => x.Response).Returns(mockResponse.Object);
    mockHttpContext.Setup(x => x.Session).Returns(sessionState);

    if(string.IsNullOrEmpty(userName))
    {
        mockHttpContext.Setup(x => x.User.Identity.IsAuthenticated).Returns(false);
        mockRequest.Setup(x => x.IsAuthenticated).Returns(false);
    }
    else
    {
        mockHttpContext.Setup(x => x.User.Identity.Name).Returns(userName);
        mockHttpContext.Setup(x => x.User.Identity.IsAuthenticated).Returns(true);
        mockRequest.Setup(x => x.IsAuthenticated).Returns(true);
    }
    mockRequest.Setup(x => x.ApplicationPath).Returns("/");

    // Build Request Context
    var ctx = new RequestContext(mockHttpContext.Object, new RouteData());
    controller.ControllerContext = new ControllerContext(ctx, controller);
    return controller;
}


사용 방법은 다음과 같다.
[Test]
public void Start()
{
    _controller = new VirtualMachineController();
    SessionStateItemCollection sessionItemCollections = new SessionStateItemCollection();
    sessionItemCollections[SessionConstants.Account] = _account;
    sessionItemCollections[SessionConstants.ZoneList] = tempZoneList;

    _session = new FakeSession(sessionItemCollections);
    _controller = GetContextedController(_controller, LocalConf.AccountName, _session);
    _controller.Start("tempVmName");
}
Posted by xyzlast Y2K
TAG mock, MVC, test
회사에서 신규 프로젝트에서 Spring.NET을 사용하기로 해서, 간단히 자료 찾아보고 공부. 끄적끄적.
개인적으로는 .NET MVC에는 Castle이 가장 어울리는 것이 아닌가 싶은데, 일단 제일 유명하고, 잘 만들어져있다는 Spring의 .NET 포팅버젼을 공부해놓는 것도 도움이 되겠다는 생각으로 시작하게 되었다.

먼저, WebService, Console Application, Web등 Context 소비자측에서 사용될 Interface를 선언한다. Interface를 통해서 구성이 되고, IoC로 구성이 될 경우 Interface의 변경은 Context 소비자의 변경을 가지고 올 수 있기 때문에 Interface의 변경에는 매우 주의를 요하게 된다.
    public interface IVirtualMachine
    {
        string CreateVM(string description, string hwProfileTextKey, string vmiName, string zoneTextKey,
            string accountName, string sequrityGroupName);
        string GetVMConfiguration(string vmName);
        string StartVM(string vmName);
        string ShutdownVM(string vmName);
        string RestartVM(string vmName);
        string StopVM(string vmName);
        string DeleteVM(string vmName);
        string SetMemorySize(string vmName, int memorySize);
        string migrateVM(string vmName, string toHostName);
        string GetVMCurrentUsage(string vmName);
        string GetPasswordData(string vmName);
    }

그리고, Interface를 상속받는 Context를 구성한다.
    public class VirtualMachine : IVirtualMachine
    {
        #region IVirtualMachine Members

        public string CreateVM(string description, string hwProfileTextKey, string vmiName, string zoneTextKey, string accountName, string sequrityGroupName)
        {
            string message = string.Format("CreateVM : {0}", description);
            return message;
        }

        public string GetVMConfiguration(string vmName)
        {
            string message = string.Format("GetVMConfiguration from Common Service : {0}", vmName);
            return message;
        }

        public string StartVM(string vmName)
        {
            throw new NotImplementedException();
        }

        public string StopVM(string vmName)
        {
            throw new NotImplementedException();
        }

        public string ShutdownVM(string vmName)
        {
            throw new NotImplementedException();
        }

        public string RestartVM(string vmName)
        {
            throw new NotImplementedException();
        }

        public string DeleteVM(string vmName)
        {
            throw new NotImplementedException();
        }

        public string SetMemorySize(string vmName, int memorySize)
        {
            throw new NotImplementedException();
        }

        public string migrateVM(string vmName, string toHostName)
        {
            throw new NotImplementedException();
        }

        public string GetVMCurrentUsage(string vmName)
        {
            throw new NotImplementedException();
        }

        public string GetPasswordData(string vmName)
        {
            throw new NotImplementedException();
        }

        #endregion
    }

Config 파일에 각 Context의 선언을 넣는다.

그리고, 사용할 소비자에서는 Spring.Context.Support.ContextRegistry 를 통해서 각 Context를 얻고, Object들을 Interface를 통해서 얻어 사용한다. 이때, 객체를 얻어와서 사용해도 되지만, 객체를 얻어오게 되면 Context의 변경이 있을때, 일관성이 떨어지고, 강한 결합으로 연결되기 때문에 n-Tier 구성에 어울리지 않게 된다. 반드시 인터페이스를 통해서 사용을 해야지 된다.

그리고, Context가 변경되게 되는 경우에는 다른 이름으로 Context를 추가하고, 정해진 Context Name을 이용해서 사용해주면 된다. 그리고, 생성자에 입력값이 있는 경우 다음과 같이 사용하면 된다.



가장 좋은 방법으로 생각되는 것은 appSettings에 사용될 Context Object Name을 추가하면 Context의 Re-Build없이 Context Object를 변경해서 사용하는 것이 가능하기 때문에 이 방법이 가장 좋을 듯 하다.



다음은 Context 소비자 코드이다.
static void Main(string[] args)
{
    string contextName = System.Configuration.ConfigurationManager.AppSettings["VirtualMachine"];
    
    IApplicationContext context = ContextRegistry.GetContext();
    IVirtualMachine vmService = (IVirtualMachine) context.GetObject(contextName);

    string result = vmService.GetVMConfiguration("YKYOON");
    Console.WriteLine(result);
}





Posted by xyzlast Y2K
사용자 가입을 하는 Form의 경우에는 일반적인 Wizard Form을 취하게 된다. Wizard Form을 통해서, 사용자 정보 수집과 각 정보에 대한 Validation check를 하게 되는데, 이 때에 너무나 많은 질문으로 인하여 당황하지 않게 하는 점진적인 표현(progressive disclosure)이라는 사용자 원칙을 따른다. 모든 질문들은 전부 서로 관련이 있는 것이 아니므로 각 단계별로 소량의 질문들을 나타내는 것이다.

여러단계를 통한 이동을 하게 되는 경우, 고려되어야지 되는 것은 Next 를 통한 입력한 정보의 전달, 그리고 Back으로 돌아갔을 때 사용자의 정보의 유지가 필요하게 된다. 이를 Session으로 처리해주는 방법도 ASP .NET MVC에서는 TempData와 데이터 Serialization을 통해서 이를 쉽게 구현 가능하다.

먼저, 사용자 데이터를 LogFormatter로 Serialization시켜주기 위한 Help method와 사용자 데이터 class를 만들어준다.

[Serializable]
public class RegistrationData
{
    public string Name { get; set; }
    public string Email { get; set; }
    public int? Age { get; set; }
    public string Hobbies { get; set; }
}

public static class SerializationUtils
{
    public static string Serialize(object obj)
    {
        using(StringWriter sw = new StringWriter())
        {
            new LosFormatter().Serialize(sw, obj);
            return sw.ToString();
        }
    }

    public static object Deserialize(string data)
    {
        if(data == null)
        {
            return null;
        }
        else
        {
            return (new LosFormatter()).Deserialize(data);
        }
    }
}

그리고, Controller의 OnActionExecuting과 OnActionExecuted 함수를 다음과 같이 재 정의 한다. 이제 Controller의 모든 Action은 TempData를 검사하고, TempData에서 데이터가 있는 경우에는 Deserialization을 통해서 사용자 데이터를 가지고 온다.

public RegistrationData RegData { get; set; }

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    //Action이 call 될 때, RegData 값을 update 시켜준다.
    RegData = (SerializationUtils.Deserialize(Request.Form["regData"]) ?? TempData["regData"] ??
        new RegistrationData()) as RegistrationData;
    TryUpdateModel(RegData);
}

protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
    if(filterContext.Result is RedirectToRouteResult)
    {
        //Back이나 Next로 RedirectToAction이 호출이 되었을때, TempData에 regData값을 넣어준다.
        TempData["regData"] = RegData;
    }
}

View에서 LogFormatter를 이용해서 데이터의 ViewState를 적어준다.


    <%using(Html.BeginForm()){ %>        
        <%=Html.ValidationSummary() %>
        <%=Html.Hidden("regData", SerializationUtils.Serialize(Model)) %>
        

Name:<%=Html.TextBox("name") %>

E-Mail:<%=Html.TextBox("email")%>

<%} %>

이와 같이 구성 이후에, Html을 보면 색다른 결과가 나온다.


    

Name:

E-Mail:


그것은 VIEWSTATE를 이용해서 Wizard Form을 구성했기 때문인데, VIEWSTATE는 ASP .NET WEBFORM기술에서 악명이 높은 기술이다. 그렇지만, 이와 같이 구성이 된다면 서버의 Session상태에 구애받지 않고, 만약에 사용자가 밤새 웹브라우저를 열어놓은 일이 있더라도 값을 보장받을 수 있는 강력한 방법이 된다.

단, 주의할점이 하나 있다. VIEWSTATE 값은 Base64로 Formatting된 값이다. 이 값의 변조는 악의적인 사용자가 가능한 정보이기 때문에, 특별한 Encoding을 통하던, 아니면 다른 방법을 통해서 암호화를 하는 것이 용의하다.
Posted by xyzlast Y2K