Visual Studio 2005는 개발자의 생산성 향상에 가장 중점을 둔 개발툴로 거듭난 것 같습니다. 이번에는 객체를 데이터 소스로 이용해 보겠습니다. 어설프게나마 어느정도 MVC 패턴을 따라가게 되었습니다.
시나리오
최근에 시나리오로 많이 쓰이는 어드벤쳐 웍스 사이클스의 데이터베이스를 이용하려다 제가 SQL Server 2005 를 설치할때 깜박한 관계로, Office 에서 제공하는 Northwind 를 이용하도록 하겠습니다. Northwind의 Order - Order Detail 테이블을 이용합니다. SQL Server 2000에서도 제공하므로 어렵지 않게 접근 할 수 있을 것입니다.
프로젝트 생성
프로젝트는, 데이터 컨트롤 프로젝트 - Controller, 데이터 객체 프로젝트 - Model, 유저 인터페이스 프로젝트 - View 를 만듭니다. Controller, Model 은 클래스 라이브러리이고, View는 윈도우 응용프로그램입니다.
MVC 패턴을 원래의 취지대로 적용하여 프로그램을 작성하기 위해서는 MVC패턴 자체에 대한 깊은 성찰이 필요한 것 같습니다. 그렇게 하기에는 좀 지루하므로 막연하게나마 마이크로소프트의 Patterns & practices 에서 제공하는 개발 스타일대로 따라해 보도록 하겠습니다.
모델
모델은 데이터에 의존적인 오퍼레이션과 비즈니스와 관련된 오퍼레이션을 담습니다. 수많은 형태로 모델이 작성되지만, 어떤것이 가장 좋은 형태인지에 대한 대답을 얻기는 쉽지 않습니다. 때에 따라 다를수 있고, 개발자의 견해에 따라 다를수 있습니다. 분명한것은 한 패턴이 정해지면, 모든 부분에서 동일한 패턴으로 프로그램이 작성되어야 한다는 것입니다.
이번 프로그램은, 각 테이블과 대응되는 컨테이너를 작성하고, 데이터 베이스에서 데이터를 불러와 컨테이너에 저장하는 형태로 가도록 하겠습니다. 관련된 데이터베이스의 테이블은 Orders와 OrderDetails가 있으므로 클래스 역시 Orders와 OrderDetails를 작성합니다. Model 프로젝트에 Orders.cs 파일과 OrderDetails 파일을 추가합니다. 각 클래스의 코드는 다음과 같습니다.
[Orders.cs]using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.OleDb;
using System.Configuration;
namespace Model
{
public class Orders
{
public static List<Orders> GetOrders()
{
OleDbDataAdapter dataAdapter;
DataSet dataSet;
List<Orders> orders;
orders = new List<Orders>();
dataSet = new DataSet();
dataAdapter = new OleDbDataAdapter("SELECT * FROM Orders", _connectionString);
dataAdapter.Fill(dataSet, "Orders");
dataAdapter = new OleDbDataAdapter("SELECT * FROM [Order Details]", _connectionString);
dataAdapter.Fill(dataSet, "OrderDetails");
foreach (DataRow drOrder in dataSet.Tables[0].Rows)
{
Orders order = new Orders();
order.OrderID = Convert.ToInt32(drOrder["OrderID"]);
order.CustomerID = Convert.ToString(drOrder["CustomerID"]);
order.EmployeeID = Convert.ToInt32(drOrder["EmployeeID"]);
order.OrderDate = Convert.ToString(drOrder["OrderDate"]);
order.RequireDate = Convert.ToString(drOrder["RequiredDate"]);
order.ShippedDate = Convert.ToString(drOrder["ShippedDate"]);
order.ShipVia = Convert.ToInt32(drOrder["ShipVia"]);
order.Freight = Convert.ToDouble(drOrder["Freight"]);
order.ShipName = Convert.ToString(drOrder["ShipName"]);
order.ShipAddress = Convert.ToString(drOrder["ShipAddress"]);
order.ShipCity = Convert.ToString(drOrder["ShipCity"]);
order.ShipRegion = Convert.ToString(drOrder["ShipRegion"]);
order.ShipPostalCode = Convert.ToString(drOrder["ShipPostalCode"]);
order.ShipCountry = Convert.ToString(drOrder["ShipCountry"]);
DataRow[] orderDetails = dataSet.Tables[1].Select(String.Format("OrderID={0}", drOrder["OrderID"]));
foreach (DataRow drOrderDetail in orderDetails)
{
OrderDetail orderDetail = new OrderDetail();
orderDetail.OrderID = Convert.ToInt32(drOrderDetail["OrderID"]);
orderDetail.ProductID = Convert.ToInt32(drOrderDetail["ProductID"]);
orderDetail.UnitPrice = Convert.ToDouble(drOrderDetail["UnitPrice"]);
orderDetail.Quantity = Convert.ToInt32(drOrderDetail["Quantity"]);
orderDetail.Discount = Convert.ToInt32(drOrderDetail["Discount"]);
order.OrderDetail.Add(orderDetail);
}
orders.Add(order);
}
return orders;
}
public static int UpdateOrders(Orders orders)
{
OleDbConnection con;
OleDbCommand cmd;
int retVal;
con = new OleDbConnection(_connectionString);
cmd = con.CreateCommand();
try
{
cmd.CommandText = "UPDATE Orders SET CustomerID=@CustomerID, EmployeeID=@EmployeeID,"
+ "OrderDate=@OrderDate, RequireDate=@RequireDate, ShippedDate=@ShippedDate, "
+ "ShipVia=@ShipVia, Freight=@Freight, ShipName=@ShipName, ShipAddress=@ShipAddress, "
+ "ShipCity=@ShipCity, ShipRegion=@ShipRegion, ShipPostalCode=@ShipPostalCode, "
+ "ShipCountry=@ShipCountry WHERE OrderID=@OrderID";
.
.
.
cmd.Parameters.Add(new OleDbParameter("@ShipName", OleDbType.VarChar));
.
.
cmd.Parameters.Add(new OleDbParameter("@OrderID", OleDbType.Integer));
.
.
.
cmd.Parameters["@ShipName"].Value = orders.ShipName;
.
.
cmd.Parameters["@OrderID"].Value = orders.OrderID;
.
.
con.Open();
retVal = cmd.ExecuteNonQuery();
}
catch (OleDbException ex)
{
throw new Exception(String.Format("데이터베이스 오류 : {0}\n", ex.Message));
}
catch (Exception ex)
{
throw new Exception(String.Format("일반 오류 : {0}\n", ex.Message));
}
finally
{
if (con.State == ConnectionState.Open) con.Close();
cmd.Parameters.Clear();
cmd = null;
con = null;
}
return retVal;
}
private List<OrderDetail> _orderDetail = new List<OrderDetail>();
public List<OrderDetail> OrderDetail
{
get { return _orderDetail; }
}
private int _OrderID;
private string _CustomerID;
private int _EmployeeID;
private string _OrderDate;
private string _RequiredDate;
private string _ShippedDate;
private int _ShipVia;
private double _Freight;
private string _ShipName;
private string _ShipAddress;
private string _ShipCity;
private string _ShipRegion;
private string _ShipPostalCode;
private string _ShipCountry;
public int OrderID
{
get { return _OrderID; }
set { _OrderID = value; }
}
public string CustomerID
{
get { return _CustomerID; }
set { _CustomerID = value; }
}
public int EmployeeID
{
get { return _EmployeeID; }
set { _EmployeeID = value; }
}
public string OrderDate
{
get { return _OrderDate; }
set { _OrderDate = value; }
}
public string RequireDate
{
get { return _RequiredDate; }
set { _RequiredDate = value; }
}
public string ShippedDate
{
get { return _ShippedDate; }
set { _ShippedDate = value; }
}
public int ShipVia
{
get { return _ShipVia; }
set { _ShipVia = value; }
}
public double Freight
{
get { return _Freight; }
set { _Freight = value; }
}
public string ShipName
{
get { return _ShipName; }
set { _ShipName = value; }
}
public string ShipAddress
{
get { return _ShipAddress; }
set { _ShipAddress = value; }
}
public string ShipCity
{
get { return _ShipCity; }
set { _ShipCity = value; }
}
public string ShipRegion
{
get { return _ShipRegion; }
set { _ShipRegion = value; }
}
public string ShipPostalCode
{
get { return _ShipPostalCode; }
set { _ShipPostalCode = value; }
}
public string ShipCountry
{
get { return _ShipCountry; }
set { _ShipCountry = value; }
}
}
}
[OrderDetails.cs]
using System;
using System.Collections.Generic;
using System.Text;
namespace Model
{
public class OrderDetail
{
public static int UpdateOrderDetail(OrderDetail orderDetail)
{
//업데이트 로직
.
.
.
.
.
}
public int _OrderID;
public int _ProductID;
public double _UnitPrice;
public int _Quantity;
public int _Discount;
public int OrderID
{
get { return _OrderID; }
set { _OrderID = value; }
}
public int ProductID
{
get { return _ProductID; }
set { _ProductID = value; }
}
public double UnitPrice
{
get { return _UnitPrice; }
set { _UnitPrice = value; }
}
public int Quantity
{
get { return _Quantity; }
set { _Quantity = value; }
}
public int Discount
{
get { return _Discount; }
set { _Discount = value; }
}
}
}
좀 유심히 봐야하는 부분은 Orders의 List<OrderDetails> 타입의 OrderDetails 멤버입니다. 테이블에서 마스터-디테일 관계에 있기때문에, OrderDetails가 Orders의 멤버로 두었습니다. 이렇게 함으로서 나중에 보실 View 의 개발시 생산성의 향상을 느낄 수 있게됩니다.
사실, 위의 코드는 수동적이든 능동적이든 코드를 자동생성 할 수 있는 부분입니다. 그리고, 가능하다면 반드시 자동생성해서 사용하기를 바랍니다.
Orders 에서는 오퍼레이션을 담당하는 static 형태의 메서드가 있는데, 이 메서드에서 실제로 컨테이너를 채우고, 채워진 컨테이너를 반환하는 역할을 합니다.
(중간에 업데이트관련코드는 왜 저모양이냐! 바쁜일정에 쫓겨서 ㅠㅜ)
컨트롤러
컨트롤러는 View와 관련되지만 분리될 수 있는 동작을 담습니다. 간단히 말해서 뷰와 모델의 다리역할을 하는 개체라고나 할까요? 모델의 데이터를 불러온다거나 업데이트를 한다거나 하는 동작을 여기에 담습니다. 보통의 어플리케이션은 MVC 패턴만으로 개발하지는 않다는 걸 여러분도 잘 알고 계실 것입니다. 업데이트시 옵저버를 이용해 다른 클라이언트에 바뀐 상태를 전달하기도 합니다. 그 외 여러가지 방법으로 많은 기능을 구현하지만 여기에서는 컨트롤러가 모든것을 다 수행하는 것으로 하겠습니다.
다음은 소스코드는 컨트롤러에 해당하는 코드입니다.
[Controller.cs]
using System;
using System.Collections.Generic;
using System.Text;
using Model;
namespace Controller
{
public class Controller
{
public static object GetOrders()
{
return (object)Orders.GetOrders();
}
public static bool UpdateOrders(Orders orders)
{
return Orders.UpdateOrders(orders) != 0;
}
public static bool UpdateOrderDetail(OrderDetail orderDetail)
{
return OrderDetail.UpdateOrderDetail(orderDetail) != 0;
}
}
}
가끔, 뷰에 들어갈 코드를 컨트롤러에 넣는 경우가 있는데, 좋은 습관은 아닌것 같습니다. 컨트롤러는 뷰와 독립되게 만들어야 활용도가 높아집니다. 즉, 결합도를 낮추는 것이지요. 실제 뷰와관련이 없는 오퍼레이션을 여기에 담는 것이 좋다는게 저의 생각입니다.
뷰
실제 고객과 인터액트하는 부분이기때문에 가장 신경을 쓴것 처럼 만들어야 하는 부분입니다. 윈폼의 경우 개발자가 Anchor 속성을 설정하는 것만 잊게되도 사용자는 크게 불편을 느낍니다. 신경을 써야 하는게 아니라 쓰는 것처럼 보이라는 이유는 실제 비즈니스 로직에 더 신경을 쓰고 구현을 해야 전체 프로젝트가 산으로 안가지만, 고객은 값만 정확히 맞는다면 이에 대해서는 관심을 크게 안가지므로, 이에대해 고객에게 떠들어봐야 "얘가 많이 힘들구나, 불쌍하다" 란 생각밖에 안하므로, 뷰부분을 신경쓰는 것 처럼 작업합니다.
다행히 Visual Studio 2005 는 이부분에서 대단한 생상성 향상을 보여줍니다. 먼저 뷰 프로젝트에 모델 프로젝트와 컨트롤러 프로젝트를 참조 추가합니다. 그 다음 뷰 프로젝트에 포커스를 맞추고, 메뉴에서 "데이터 > 새 데이터 소스 추가" 를 실행합니다.

그림1과 같은 다이얼로그 창이 나타나는데 "개체"를 선택하고 다음을 누릅니다.

그러면, 그림2와 같이 바인딩할 개체를 선택하는 화면이 나오는데, 프로젝트 참조를 하셨다면 모델 프로젝트가 보일 것입니다. 여기서 Model 네임스페이스의 Orders 클래스를 선택합니다. OrderDetail은 선택할 필요가 없는 것이 어짜피 Orders가 OrderDetail 클래스를 포함하기 때문에 따로 선택할 필요는 없습니다.
다음을 누러 데이터 소스 추가를 완료합니다. 그러면 데이터 소스창에 보면, Orders 개체가 추가된것이 보입니다. 데이터 소스창이 보이지 않는다면, Shift + Alt + D 키를 눌러 화면에 표시합니다.
그럼 데이터 소스창에는 다음과 같은 화면이 보입니다.

Orders 테이블이 보이고, OrderDetail 테이블이 보입니다. 실은 Orders 클래스와 OrderDetail 클래스를 가리킵니다. VS2005 가 알아서 어울리는 컨트롤로 맵핑을 했지만, 개발자가 어울리는 컨트롤을 지정할 수 있습니다.
일단을 각 필드를 폼위에 끌어다 놓습니다. 그럼, 도구상자에서 컨트롤을 끌어다 놓는 것과 같이 폼위에 컨트롤이 그려지게 되는데, 다른점이 있다면, 이렇게 하면 각 컨트롤은 자동으로 데이터 소스와 바운딩이 됩니다.
다음은 그 화면입니다. 시간상 SplitContainer 컨트롤에 (데이터 소스의)데이터 그리드 두개, FlowLayoutPanel 두개 올려놓았습니다. FlowLayoutPanel 에는 상세보기를 위해 각 필드를 올려 놓습니다.
다음은 그 화면입니다.

조금 잘리긴했지만, 충분히 이해하시라 생각합니다. 잘 보셔야 할 것은 밑의 바운딩 소스입니다. Orders, OrderDetail 이 관계된 바운딩 소스인데 이 둘은 연결이 되어있습니다. 그래서 Orders 에만 데이터 소스를 대입하면 OrderDetail에까지 데이터가 연결 됩니다.
다음은 뷰에 해당하는 코드입니다.
[View.cs]
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Model;
namespace View
{
public partial class View : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
ordersBindingSource.DataSource = Controller.Controller.GetOrders();
}
private void btnSave_Click(object sender, EventArgs e)
{
ordersBindingSource.EndEdit();
if (!Controller.Controller.UpdateOrders((Orders)ordersBindingSource.Current))
MessageBox.Show("업데이트 실패!");
}
private void btnDetailSave_Click(object sender, EventArgs e)
{
orderDetailBindingSource.EndEdit();
if (!Controller.Controller.UpdateOrderDetail((OrderDetail)orderDetailBindingSource.Current))
MessageBox.Show("업데이트 실패!");
}
}
}
저장 방법이 내가 데이터를 다루는 동안에는 다른 사람이 데이터를 다루지 않는다고 가정하는 낙관정 동시 접근 방법을 사용하기 때문에 좀 위험한 부분도 있지만 아주 간단히 기본적인(아주 부족한) 어플리케이션이 완성 되었습니다.
실행시켜 보시면 데이터 그리드에서 데이터를 마우스로 선택하면 관련된 컨트롤의 데이터까지 해당하는 데이터로 채워진다는 것입니다. 우리는 어떤 코드도 입력 안했는데 말이죠. 뭐, 당연한 건가요^^
정리
Visual C# 1.1 에서는 데이터메니저를 클래스 형태로 제공하긴했지만, 바운딩 소스 컴포넌트가 더 편한 것 같습니다. 생산성을 극대화 시켜주는 느낌입니다. 다음에는 좀더 정교하게 컨트롤에 되는 어플리케이션을 다루어 보겠습니다.
- High-Performance .NET Application Development &... (0)2007/04/29
- 데이터 바인딩 어플리케이션 만들기 기초 (0)2007/01/11
- 개체를 이용한 데이터 바인딩 어플리케이션 만들기 (0)2007/01/11
- 닷넷 윈폼 배포시 Framework, mdac 등 병합모듈로... (0)2007/01/11
- Windows Form : 윈도우 폼 꾸미기 (0)2007/01/11

수안이의 컴퓨터 연구실



Leave your greetings.