2005-12-11

ADO.NET 팁

ADO.NET + VB.NET 으로 게임 상점 관리툴을 개발하면서 얻은 팁들을 정리해보았다.

현재 선택된 셀 알아내기

그리드에서 현재 선택된 셀을 알아내려면 DataGrid.CurrentCell 또는 DataGrid.CurrentRowIndex 를 사용한다. 그런데, 마우스가 그리드 프레임 또는 외부에 존재할 경우 값이 부정확하게 나온다. 따라서, DataGrid.HitTest(mousepos) 를 사용해서 마우스 좌표를 그리드 좌표로 변환한 다음 바운드 체크를 해야 한다.

스키마 변경시 해야 할 일들

DB 관련 작업을 하다 보면 불가피하게 스키마를 변경해야 할 때가 있다. 그때마다, 기존의 VB.NET 프로젝트에 만들어져 있는 형식화된 DataSet과 DataAdapter를 재설정해야만 낭패를 면할 수 있다.

  • 방법은 간단한데, 디자이너 모드에서 각 DataAdapter를 선택하고 오른쪽 클릭해서, "데이터 어댑터 구성"을 선택해서 새로 SQLCommand 들을 생성해주면 된다.
  • 그다음 "데이터 집합 생성"을 선택하면 된다.
  • 끝나고 나서 DataSet 객체를 오른쪽 클릭해서, "데이터 집합 보기" 를 선택한 후 잘 바뀌었는지 체크해본다. (때로는, FK 문제로 에러가 나기도 하지만 무방하다)
  • 틈틈히 스키마가 담겨있는 xsd 파일을 체크해서 최신 스키마로 되어 있는지를 확인해주면 더욱 좋다.

관계 지정

데이터 그리드에서 관련된 row 를 보여주고 싶다면 DataRelation 을 설정하면 된다.

  • 프로그래밍 방식 : DataRelation 객체를 생성해서 부모 테이블의 컬럼과 자식 테이블의 컬럼을 지정할 수 있다. (비츄)

' ProductInfo.ProductID = ProductItems.ProductID
Dim FK_ProductItems_ProductInfo As DataRelation = New DataRelation( "FK_ProductItems_ProductInfo", _
dsBoom.ProductInfo.ProductIDColumn, dsBoom.ProductItems.ProductIDColumn)
FK_ProductItems_ProductInfo.Nested = True
dsBoom.Relations.Add(FK_ProductItems_ProductInfo)

  • 디자이너 방식 : 스키마 에디터에서 컬럼 끼리 연결짓고 이름을 명시하면 된다. (추천)

  • DataRelation.Nested 속성은 디자이너 모드에서 속성 화면에서 수정하면 된다.

  • 만약, 상위 혹은 하위 테이블의 내용을 보여주고 싶지 않다면 DataGrid.AllowNavigation = False 로 지정하라.
  • 네비게이션 상태에서의 테이블 스타일은 명시적으로 설정하려면, 해당 DataGrid 에 테이블 스타일을 하나 더 추가하면 된다.
  • DataRelation의 이름이 한글이어도 무방하다. 단 row 마다 다르게 줄 수는 없는 듯하다. (가령 하위 테이블 row 의 개수를 이름 뒤에 붙이고 싶어도..포기하라..)

DataSet.Merge

데이터 집합의 개념상 메모리상의 캐시를 갖고 이리 저리 갖고 놀다가 최종적인 변화를 DB로 업데이트하게 된다. 보통 이런 작업은 그리드의 특정 row 를 클릭하면 해당 정보를 보여주는 새로운 윈도우 폼이 뜨고, 여기에서 수정을 하고 저장하면 원래 데이터 집합에 반영시키면 된다는 거다.

이때 주의해야 할 점은.. DataSet.AcceptChanges() 와 DataSet.RejectChanges() 는 DB에서 읽어온 후로의 변화를 커밋하거나 롤백시킨다는 점이다. 즉 이 함수 중 하나를 일단 부르게 되면, 그동안 있었던 모든 변화가 날아가거나 더이상 롤백시킬 수 없는 상황에 이르게 된다.

만약, 에디트 폼의 수많은 컨트롤들과 원본 데이터집합을 DataView 를 통해서 바인딩시킬 경우, 에디트 폼에서 수정을 하다가 캔슬시켜도 원본 데이터집합이 변형되게 된다. 그렇다고 Reject 를 호출하게 되면 그전의 수정 사항이 모두 날아가게 되니... 대략 난감한 상황이 된다.

레이옷이 추천하는 방식은, 새 에디트 폼에 원본 데이터집합의 복사본(DataSet.Copy())을 넘겨주고 에디트 폼에서 수정한 후 OK 하면 Merge 하고 Cancel 하면 그냥 무시하는 방식이다.

관련 테이블의 내용도 함께 보여주는 방법

특정 상품에 포함된 아이템 정보를 보여준다고 하자. DB 상에서는 이를 상품-아이템 관계 테이블로 구현하게 된다. 이때, DB 에서 읽어오는 값은 보통 정수형 상품키/아이템키 값일 뿐이며, 이를 그냥 바인딩시키면 그리드에는 숫자만 나오게 되므로 대략 알아보기가 곤란하다. ( 3번 아이템 2개와 5번 아이템 1개로 구성된 상품.. 직관적이지 않다)

이를 해결하는 방법은 여러 가지가 있는 듯한데, 본좌 1개의 방법만 발견했다. 그것은, 아예 데이터 아답타 레벨에서 join 을 걸든 join 된 view 를 이용하든, 키값과 이름을 함께 읽어온다는 거다.

이 방식의 단점은, 데이터 아답타 마법사에서 UpdateCommand와 DeleteCommand 를 자동으로 생성하지 못한다는데 있다. 결국 이 부분은 수동으로 해줘야 하는데... 난감하다.

일단 다른 아답타의 Update/Delete Command 에서 샘플을 보고 참고해서 만들어나가면 되긴 된다. 이때, 파라미터가 잘 만들어졌는지, Current/Original 인지 확인해줄 것

주의사항 : 뷰를 통해서 데이터를 갖고 올 경우, 데이터 집합 생성시 xxxView 로 만들어진다. 이를 원래의 관계 테이블로 매핑시키려면, DataAdapter.TableMapping 속성을 설정해주면된다.

입력 검증(verification) 과 이벤트 핸들링

만약 그리드에서 on the fly 수정을 하려면, 데이터 테이블에서 발생시키는 이벤트를 핸들링시켜야 한다. 총 6가지의 이벤트가 존재한다.

  • ColumnChanging -> ColumnChanged
  • RowChanging -> RowChanged
  • RowDeleting -> RowDeleted

이런 이벤트들은 대략 다음과 같은 조건에서 발생한다.

  • 특정 셀을 수정한 다음 column 이 바뀌면(==focus changed) ColumnChanging -> ColumnChanged 순서로 호출된다.
  • row 가 바뀌면 ColumnChanging -> ColumnChanged -> RowChanging -> RowChanged 순서로 호출된다.

상점 관리툴에서는, 그리드에서 수정을 하게 되면 저장 버튼을 활성화 시켜서 뭔가가 바뀌었다고 사용자에게 알려주도록 하기 위해, xxxChanged 이벤트 핸들러에서 대략 다음과 같은 코드를 사용했다.

Private Sub OnCategoryRowChanged(ByVal sender As Object, ByVal args As DataRowChangeEventArgs)
SaveButton.Enabled = dsBoom.HasChanges
End Sub

실제로 이벤트 핸들러에서는 현재 유저가 수정한 내용을 보고, 범위에 맞는지 혹은 적절한 값인지를 검사해서 틀릴 경우 예외를 던지는 방식의 입력 검증을 해줘야 한다.

데이터 그리드 간 연동

데이터베이스 상에 MasterTable, SlaveTable 이라는 2개의 테이블이 dsXXX 라는 데이터셋에 dlXXX 라는 relation 으로 연관되어 있다고 하자. 이 테이블들을 각각 MasterGrid, SlaveGrid 에서 보여주려고 할 때, 아래와 같이 설정하면 두 그리드가 연동된다.

MasterGrid.DataSource = dsXXX
MasterGrid.DataMember = "MasterTable"

SlaveGrid.DataSource = dsXXX
SalveGrid.DataMember = "MasterTable.dlXXX"

이때 주의할 점은, SlaveGrid 와 연관된 SlaveTable 의 실제 크기와 보여지는 크기가 다르다는 것이다. (FK로 필터링되었으니 출력되는 크기는 실제 크기보다 작거나 같게 된다) 따라서, 마우스로 특정 row 를 선택했을 때, MasterGrid 의 경우 데이터테이블을 통해서 직접 접근하면 되지만, SlaveGrid 의 경우에는 이를 알 수 없다는 것이 문제가 된다. (특히 VisibleRowCount 는 단지 현재 화면에 보이는 크기라서 신용할 수 없다)

이 경우에는 현재 SlaveGrid 의 CurrentRowIndex 라든지 HitTestInfo 를 써서 현재 row 를 알아낸 다음, MasterTable(MasterGrid.CurrentRowIndex).GetChildRows("dlXXX") 를 통해서 실제 값에 접근하면 된다. 이 GetChildRows(relation_name) 은 DataGrid.AllowNavigation = True 를 설정했을 때 나오는 놈들과 같다.

다이얼로그 with 데이터집합

단지 책을 통해서 얻은 지식만으로 한 거라서 더 좋은 방법이 있을지도 모르겠지만, 폼에서 작업을 하다가 어떤 상황이 되어 다이얼로그를 띄워야 할 때, 그 다이얼로그에서도 현재 데이터셋과 동일한 넘을 Clone() 이든 Copy() 든 복제해서 써야 할 경우가 있다.

예를 들면, 특정 상품 카테고리에 새 상품을 추가할 때, 전체 상품 목록 중에서 이미 해당 카테고리에 들어가 있는 상품을 제외한 나머지 목록이 데이터 그리드에 담긴 새 다이얼로그를 띄워야 하는 경우가 있을 수 있다. (지금 하고 있는 작업이다) 이를 위해서는

i. 새 다이얼로그에 데이터셋을 추가
i. 데이터 그리드 하나를 추가
i. 디자이너에서 데이터그리드의 데이터소스 및 데이터멤버를 지정
i. 코드에서 다이얼로그 객체를 생성하고 부모 폼에서 데이터셋을 복사한 다음 다이얼로그를 띄우기

Dim dlg As ProductSelectDialog = new ProductSelectDialog
dlg.dsXXX = Me.dsXXX.Copy
dlg.ShowDialog()

이렇게 해도, 새 다이얼로그는 빈 그리드만을 담게 된다. 이유는 단 하나. 디자이너 모드에서 지정한 데이터소스 및 데이터멤버가 Copy() 에 의해서 초기화되었기 때문이다. (너무나도 사소한 버그) 이를 해결하려면 프로그래밍방식으로 지정하는 수 밖에 없다.

Dim dlg As ProductSelectDialog = new ProductSelectDialog
dlg.dsXXX = Me.dsXXX.Copy
dlg.DataGrid1.DataSource = dlg.dsXXX
dlg.DataGrid1.DataMember = "what"
dlg.ShowDialog()

마우스로 그리드 위치 알아내기

물론 DataGrid.CurrentRowIndex 라든지 CurrentCell 이 좋을 것 같지만, MouseUp 이벤트 핸들러에서 얘들은 그리드 바깥쪽을 클릭했을 때 이전에 선택된 값을 리턴하므로 부정확한 정보를 리턴하게 된다. 따라서 HItTestInfo 를 사용하는 것을 추천한다.

Dim pos As DataGrid.HitTestInfo = MyGrid.HitTest(e.X, e.Y)

그런데, 이렇게 값을 얻어와서 선택된 row 의 실제 값을 알아오려면 바운드 체크가 필수적이다. 만약 그리드에서 row header, column header 를 출력하고 있다면 유저는 헤더를 선택하기가 쉬운데, 이때 각 값은 -1 을 리턴하게 되고 이 값으로 데이터테이블에 접근하면 낭패가 된다. 즉, pos.Row > 0 And pos.Column > 0 체크는 필수적이며, 가능하면 바인딩된 데이터 테이블의 최대 크기와도 비교해주면 좋을 것이다. 덤으로, DataGrid.Select(row) 를 해주면 현재 row 전체의 배경색을 반전시킬 수 있다.

새 row 의 특정 셀에 값 넣기

그리드의 마지막 row 를 클릭하면 임시 row 가 새로 만들어지고, 여기에 값을 적당히 넣고 포커스를 다른 곳으로 옮기면 이넘이 그리드에 바인딩된 데이터 테이블로 커밋된다.

문제는, 이 넘은 커밋되기 전까지는 데이터 테이블에서 검색할 수 없다는 점이다. 웬지 이 row 의 상태는 Detached 인 거 같지만, DataTable.Select() 에서는 이 상태로 검색을 지원하지 않는다. 당연히 아직 커밋되지 않았으므로 CurrentRows 로도 찾을 수는 없다.

해결책은, DataGrid.Item(row,col) 을 이용해서 직접 수정하면 된다는 것. 지금까지는 무조건 데이터테이블을 통해서 실제 데이터에 접근했는데, 앞으로는 그리드를 통해서 데이터에 접근하는 것도 좋은 방법이 될 듯하다.

특정 셀에 DBNull 넣기

원하는 셀에 포커스를 맞춘 다음, Ctrl + 0 를 입력하면 DBNULL 로 초기화된다.

output parameter 가 있는 프로시저 실행

코드에서 SQLCommand 를 직접 사용하면 된다. 코드는 매우 직관적이며 간단하지만, 한가지 주의할 점이 있다.

실제 프로시저에서 명시한 것과 동일한 이름의 파라미터를 사용하라!!

Dim cmd As SqlCommand = New SqlCommand
cmd.CommandText = "proc_name"
cmd.Connection = conn
cmd.CommandType = CommandType.StoredProcedure

Dim param As SqlParameter = cmd.Parameters.Add("@in_param", SqlDbType.VarChar, 11)
param.Value = "something"

param = cmd.Parameters.Add("@out_param", SqlDbType.Int)
param.Direction = ParameterDirection.Output

conn.Open()

Dim reader As SqlDataReader
reader = cmd.ExecuteReader()
reader.Read()
reader.Close()
conn.Close()

TextBox1.Text = cmd.Parameters("@out_param").Value


comments powered by Disqus