지금까지 작성한 코드는 다음 링크를 통해서 다운로드받을 수 있다.
이제 메뉴를 출력해보자. 그러자면 우선 메뉴 소스가 데이터베이스든 XML이든 구성되어 있어야 할 것이다. 그러나 여기서는 우선 아주 쉬운 방법을 택하겠다. 하드코딩 !
메뉴 소스가 구성되어 있다면 그것을 읽어들여 메뉴 계층 구조를 만들겠다. 이 계층구조는 눈으로 볼 수 있는 트리구조가 아니다. 단지 논리적인 메뉴 객체들의 트리구조이다. 따라서 ‘논리적인 메뉴 구조를 구성’한다고 표현할 수 있겠다. 메뉴의 논리적인 트리 구조를 만들기 위해서 필요한 클래스를 만들어보자.
우선 메뉴 아이템 정보를 나타내는 클래스를 만들었다.
메뉴 아이템을 나타내는 클래스들은 Dalbong2.Win 프로젝트에 포함시켰다. 이 녀석들은 현장의 프로젝트와는 상관없기때문이다.
메뉴 트리를 구성하는 아이템들은 윈도우 탐색기의 트리구조의 각 항목들에 해당한다고 볼 수 있다 : 드라이브, 폴더, 최종 파일.
달봉이는 파일 시스템의 각각의 노드에 해당하는 메뉴 클래스를 만들어서 메뉴 아이템의 각 항목을 표현하도록 했다 : DriveMenuItemInfo, FolderMenuItemInfo, FileMenuItemInfo.
그리고도 RootMenuItemInfo, MenuItemInfo, IMenuItemInfo를 추가적으로 만들었다. RootMenuItemInof는 추후 메뉴 구조가 컨트롤로 출력될때 보이지는 않겠지만 논리적인 트리 구조를 만들기 위한 녀석이다. 그리고 MenuItemInfo은 Drive, Folder, File 그리고 Root 메뉴 정보 클래스의 부모 클래스이다. 그리고 IMenuItemInfo는 메뉴 정보 객체라면 노출해야 하는 속성과 메소드를 정의해놓은 인터페이스이다.
메뉴 아이템 클래스들은 다음과 같이 구성된 테이블을 염두에 두고 만들었다.
코드를 보자.
public interface IMenuItemInfo
{
string ID {get;}
string Name {get;}
IMenuItemInfo ParentMenuItemInfo
{
get;
set;
}
PropertyCollection ExtraPropertyValues
{
get;
set;
}
void AddChild(IMenuItemInfo itemInfo);
List<IMenuItemInfo> GetChildren();
}
각 메뉴 아이템은 고유한 아이디와 그리고 이름이 있다. 그리고 메뉴 아이템 각각은 부모 아이템을 알고 있도록 했고, ParentMenuItemInfo 속성을 통해서 설정할 수 있고 조회할 수 있다. 그리고 자신에 속한 자식 메뉴 아이템을 외부에 제공할 수 있어야 한다. 이것은 GetChildren()이 담당한다. 그리고 자식 메뉴 정보를 추가할 수 있는 API가 있도록 했다 : AddChild(). ExtraPropertyValues 라는 PropertyCollection 타입의 속성이 있다. 메뉴 관리 프로그램에서 관리하는 정보가 각 현장의 사이트마다 달라질 수 있을 것이다. 이런 정보는 모두 ExtraPropertyValues 속성에 저장될 것이다. 그 값을 참조할때는 다음과 같은 형식으로 할 수 있다.
itemInfo.ExtraPropertyValues["ColumnName"].ToString();
MenuItemInfo 클래스는 기본적인 IMenuItemInfo를 구현해놓은 추상클래스이다.
public abstract class MenuItemInfo : IMenuItemInfo
{
string _ID = "";
string _Name = "";
IMenuItemInfo _ParentMenuItemInfo = null;
List<IMenuItemInfo> _Childern = null;
PropertyCollection _ExtraPropertyValues = null;
public MenuItemInfo(string id, string name)
{
_ID = id;
_Name = name;
}
public string ID
{
get { return _ID; }
}
public string Name
{
get { return _Name; }
}
public IMenuItemInfo ParentMenuItemInfo
{
get { return _ParentMenuItemInfo; }
set { _ParentMenuItemInfo = value; }
}
public void AddChild(IMenuItemInfo itemInfo)
{
itemInfo.ParentMenuItemInfo = this;
if (_Childern == null)
_Childern = new List<IMenuItemInfo>();
_Childern.Add(itemInfo);
}
public List<IMenuItemInfo> GetChildren()
{
return _Childern;
}
public PropertyCollection ExtraPropertyValues
{
get { return _ExtraPropertyValues; }
set { _ExtraPropertyValues = value; }
}
}
특별한 것은 없다. 다만 눈여겨 볼 것은 AddChild()를 이용해서 자식 메뉴 아이템을 추가할 때 부모 아이템으로 자신을 지정해주는 것 정도가 있다.
다음은 RootMenuItemInfo 다
public class RootMenuItemInfo : MenuItemInfo
{
public RootMenuItemInfo(string id, string name) : base( id, name)
{
base.ParentMenuItemInfo = null;
}
}
다음은 DriveMenuItemInfo, FolderMenuItemInfo
public class DriveMenuItemInfo : MenuItemInfo
{
public DriveMenuItemInfo(string id, string name ) : base(id, name)
{
}
}
public class FolderMenuItemInfo : MenuItemInfo
{
public FolderMenuItemInfo(string id, string name) : base(id, name)
{
}
}
다음은 FileMenuItemInfo. 이 녀석은 메뉴 구조의 마지막 레벨의 메뉴 아이템을 나타내는 녀석으로서 이 녀석이 나타내는 컨트롤을 사용자가 클릭하게 되면 화면이 뜨게 되는 것이다. 따라서 이 녀석은 화면 객체 정보 Dalbong2ElementInfo를 가지고 있어야 한다.
public class FileMenuItemInfo : MenuItemInfo
{
private Dalbong2ElementInfo _Dalbong2ElementInfo = null;
public FileMenuItemInfo(string id, string name)
: base(id, name)
{
}
public FileMenuItemInfo(string id, string name, Dalbong2ElementInfo dalbong2ElementInfo)
: base(id, name)
{
_Dalbong2ElementInfo = dalbong2ElementInfo;
}
public Dalbong2ElementInfo Dalbong2ElementInfo
{
get { return _Dalbong2ElementInfo; }
set { _Dalbong2ElementInfo = value; }
}
}
이제 이 녀석들을 이용해서 논리적인 메뉴 구조를 구성할 녀석을 만들어보자. 이 녀석은 MenuHelper라는 이름으로 만들었다. MenuHelper는 현장의 프로젝트 프레임워크에 추가했다. 다음 그림은 가상의 BONG이라는 가상 회사의 프레임워크를 담당하는 부분이다.
이 녀석부터는 현장의 요구에 따라서 구현이 달라질 수 있을 것이라는 생각때문이다. 예를 들어 XML로 메뉴를 구성할 수도 있겠고, 데이터베이스로 메뉴를 구성할 수도 있을 것이다( 물론 대부분의 기업형 애플리케이션은 데이터베이스로 관리한다).
public static class MenuHelper
{
//기본적인 컬럼네임
public const string MenuIDColumnName = "ID";
public const string MenuNameColumnName = "Name";
public const string MenuTypeColumnName = "MenuType";
public const string ElementIDColumnName = "ClassName";
public const string FullyQualifiedTypeNameColumnName = "ClassName";
public const string FileNameColumnName = "FileName";
public const string LoadUrlColumnName = "LoadUrl";
public const string ParentMenuIDColumnName = "parentid";
public const string RootMenuID = "ROOT";
public const string RootMenuTypeCode = "ROOT";
public const string DriveMenuTypeCode = "DRIVE";
public const string FolderMenuTypeCode = "FOLDER";
public const string FileMenuTypeCode = "FILE";
private static List<Dalbong2ElementInfo> _Dalbong2ElementInfos = null;
public static RootMenuItemInfo ConstructLogicalMenuItemInfoTree(DataSet dsMenuInfo)
{
RootMenuItemInfo rootMenuInfo = new RootMenuItemInfo(RootMenuID, RootMenuID);
//DataSet 파싱
ProcessMenuInfo(rootMenuInfo,dsMenuInfo.Tables[0]);
return rootMenuInfo;
}
private static void ProcessMenuInfo( IMenuItemInfo parentItemInfo, DataTable dtMenuInfo)
{
IMenuItemInfo menuItemInfo = null;
Dalbong2ElementInfo dalbong2ElementInfo = null;
PropertyCollection propertis = null;
DataRow[] drs = GetChildernOf(parentItemInfo.ID, dtMenuInfo);
if (drs == null) return;
for (int i = 0; i < drs.Length; i++)
{
//MenuItemInfo 객체 생성
switch( drs[i][MenuTypeColumnName].ToString() )
{
case DriveMenuTypeCode :
menuItemInfo = new DriveMenuItemInfo(drs[i][MenuIDColumnName].ToString(), drs[i][MenuNameColumnName].ToString());
break;
case FolderMenuTypeCode:
menuItemInfo = new FolderMenuItemInfo(drs[i][MenuIDColumnName].ToString(), drs[i][MenuNameColumnName].ToString());
break;
case FileMenuTypeCode:
menuItemInfo = new FileMenuItemInfo(drs[i][MenuIDColumnName].ToString(), drs[i][MenuNameColumnName].ToString());
dalbong2ElementInfo = new Dalbong2ElementInfo(
drs[i][ElementIDColumnName].ToString(),
drs[i][FullyQualifiedTypeNameColumnName].ToString(),
drs[i][FileNameColumnName].ToString(),
drs[i][LoadUrlColumnName].ToString());
((FileMenuItemInfo)menuItemInfo).Dalbong2ElementInfo = dalbong2ElementInfo;
if (_Dalbong2ElementInfos == null)
_Dalbong2ElementInfos = new List<Dalbong2ElementInfo>();
//Dabong2ElementInfo 객체는 다음에 Dalbong2XmlObjectFactory에 넘겨줄것을 대비해서
//미리 별도로 저장해둔다.
_Dalbong2ElementInfos.Add(dalbong2ElementInfo);
break;
default :
throw new Exception(string.Format("This menu type is not defined.type:{0}, menuid:{1}",
drs[i][MenuTypeColumnName].ToString(), drs[i][MenuIDColumnName].ToString()));
}
if( menuItemInfo != null)
parentItemInfo.AddChild( menuItemInfo );
//기타 추가된 메뉴 정보는 ExtraPropertyValues에 (컬럼명, 값)쌍으로 추가한다.
foreach( DataColumn ds in dtMenuInfo.Columns )
{
if( propertis == null )
propertis = new PropertyCollection();
switch( ds.ColumnName )
{
case MenuIDColumnName :
break;
case MenuNameColumnName:
break;
case MenuTypeColumnName:
break;
case FullyQualifiedTypeNameColumnName:
break;
case FileNameColumnName:
break;
case LoadUrlColumnName:
break;
case ParentMenuIDColumnName:
break;
default:
propertis.Add( ds.ColumnName, drs[i][ds.ColumnName] );
break;
}
}
if( propertis != null)
menuItemInfo.ExtraPropertyValues = propertis;
//재귀호출
ProcessMenuInfo(menuItemInfo, dtMenuInfo);
}
}
private static DataRow[] GetChildernOf(string parentID, DataTable dt)
{
DataRow[] drs = dt.Select(string.Format(ParentMenuIDColumnName + "='{0}'", parentID));
return drs;
}
public static List<Dalbong2ElementInfo> GetDalbong2ElementInfos()
{
return _Dalbong2ElementInfos;
}
}
MenuHelper 클래스는 static으로 정의했고 모든 멤버들은 static이다. 외부로 노출된 것은 두개가 있다.
public static RootMenuItemInfo ConstructLogicalMenuItemInfoTree(DataSet dsMenuInfo)
public static List<Dalbong2ElementInfo> GetDalbong2ElementInfos()
첫번째 녀석은 메뉴 정보 테이블을 가지고 있는 DataSet 객체를 받아서 논리적으로 트리 구조를 구성한 후 그 트리구조의 루트를 반환한다. 호출하는 코드에서는 루트에 대한 참조만 알고 있으면 모든 메뉴 구조를 찾아갈 수가 있다 : 각 메뉴 아이템은 GetChildren() 메소드를 통해서 자신의 자식들을 반환해줄 수 있기때문이다.
그리고 MenuHelper에서는 메뉴 구조를 구성하면서 화면 정보 객체 Dalbong2ElementInfo 리스트도 구성한다. 이렇게 구성된 화면 정보 리스트 객체 List<Dalbong2ElementInfo>는 이전 포스트에서 본 것처럼 Dalbong2XmlObjectFactory의 RegisterDalbong2ElementDefinitions()에 건네져서 화면 정보를 로딩하는데 사용된다.
_Dalbong2XmlObjectFactory.RegisterDalbong2ElementDefinitions(_Dalbong2ElementInfos);
이제 간단히 UI 컨테이너를 구성해보자. UI 컨테이너란 앞에서 말한 것처럼 사용자에게 보여질 모습을 구성하는 곳이다. 이곳에 메뉴가 출력되고, 화면이 달린 메뉴를 클릭하면 해당 화면이 출력되는 비주얼한 컨테이너이다.
이 프로그램은 당근 WPF 애플리케이션으로 구성된다. Shell.xaml을 하나 추가했다. Shell.xaml의 디자인은 다음처럼 되어 있다.
좌측 컬럼에 트리 컨트롤을 사용해서 메뉴 트리가 출력될 것이다.
그리고 xaml 코드는 다음과 같다.
<Window x:Class="BongApp.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Shell" Height="200" Width="250">
<Grid ShowGridLines="True" SnapsToDevicePixels="True ">
<Grid.RowDefinitions>
<RowDefinition Height="25*" />
<RowDefinition Height="137*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48*" />
<ColumnDefinition Width="180*" />
</Grid.ColumnDefinitions>
<TabControl Grid.Column="1" Grid.Row="1" Name="tabControl1">
<TabItem Header="tabItem1" Name="tabItem1" Loaded="tabItem1_Loaded">
<Grid />
</TabItem>
</TabControl>
<TreeView Grid.Row="1" Name="tvMenuTree" />
<DockPanel Grid.Row="0" Grid.Column="1" Name="dpHeader">
<StackPanel Name="spHeaderContent" />
</DockPanel>
</Grid>
</Window>
다음은 코드 비하인드 파일의 내용이다.
public partial class Shell : Window
{
public Shell()
{
InitializeComponent();
}
Dalbong2XmlObjectFactory _Dalbong2XmlObjectFactory = null;
private void tabItem1_Loaded(object sender, RoutedEventArgs e)
{
//메뉴 정보를 조회한다.
DataSet dsMenuItemInfos = GetMenuItemInfos();
//메뉴의 로지컬 트리를 구성한다.
RootMenuItemInfo rootMenuInfo = MenuHelper.ConstructLogicalMenuItemInfoTree(dsMenuItemInfos);
//Dalbong2Element 정보를 구한다.
List<Dalbong2ElementInfo> _Dalbong2ElementInfos = MenuHelper.GetDalbong2ElementInfos();
//메뉴를 비쥬얼하게 디스플레이한다.
DisplayVisualTree(tvMenuTree, rootMenuInfo);
//Dalbong2Element 객체 정보 등록
_Dalbong2XmlObjectFactory = new Dalbong2XmlObjectFactory(new ConfigSectionResource("config://spring/objects"));
_Dalbong2XmlObjectFactory.RegisterDalbong2ElementDefinitions(_Dalbong2ElementInfos);
}
Shell.xaml이 로딩되면서 실행되는 코드이다.
먼저 GetMenuItemInfos()를 호출해서 메뉴 정보를 메뉴 소스에서 가져온다. 현재는 하드 코딩했다.
/// <summary>
/// 내부에서는 실제로 데이터베이스를 호출해서 메뉴 정보를 가져온다.
/// </summary>
/// <returns></returns>
private DataSet GetMenuItemInfos()
{
DataSet ds = null;
ds = GetMenuItemInfos_IMSI();
return ds;
}
private DataSet GetMenuItemInfos_IMSI()
{
DataSet ds = new DataSet();
DataTable dt = new DataTable();
ds.Tables.Add(dt);
dt.Columns.Add(new DataColumn(MenuHelper.MenuIDColumnName));
dt.Columns.Add(new DataColumn(MenuHelper.MenuNameColumnName ));
dt.Columns.Add(new DataColumn( MenuHelper.MenuTypeColumnName));
dt.Columns.Add(new DataColumn(MenuHelper.FullyQualifiedTypeNameColumnName));
dt.Columns.Add(new DataColumn(MenuHelper.FileNameColumnName));
dt.Columns.Add(new DataColumn(MenuHelper.LoadUrlColumnName));
dt.Columns.Add(new DataColumn(MenuHelper.ParentMenuIDColumnName));
DataRow dr = null;
//루트 메뉴 추가
dr = dt.NewRow();
dr[MenuHelper.MenuIDColumnName] = MenuHelper.RootMenuID;
dr[MenuHelper.MenuNameColumnName] = "ROOT";
dr[MenuHelper.MenuTypeColumnName] = MenuHelper.RootMenuTypeCode;
dt.Rows.Add(dr);
//레벨 1 메뉴 추가
dr = dt.NewRow();
dr[MenuHelper.MenuIDColumnName] = "01";
dr[MenuHelper.MenuNameColumnName] = "01 드라이브";
dr[MenuHelper.MenuTypeColumnName] = MenuHelper.DriveMenuTypeCode ;
dr[MenuHelper.ParentMenuIDColumnName] = MenuHelper.RootMenuID;
dt.Rows.Add(dr);
dr = dt.NewRow();
dr[MenuHelper.MenuIDColumnName] = "02";
dr[MenuHelper.MenuNameColumnName] = "02 드라이브";
dr[MenuHelper.MenuTypeColumnName] = MenuHelper.DriveMenuTypeCode;
dr[MenuHelper.ParentMenuIDColumnName] = MenuHelper.RootMenuID;
dt.Rows.Add(dr);
// 01드라이브의 폴더
dr = dt.NewRow();
dr[MenuHelper.MenuIDColumnName] = "0101";
dr[MenuHelper.MenuNameColumnName] = "01 폴더";
dr[MenuHelper.MenuTypeColumnName] = MenuHelper.FolderMenuTypeCode;
dr[MenuHelper.ParentMenuIDColumnName] = "01";
dt.Rows.Add(dr);
dr = dt.NewRow();
dr[MenuHelper.MenuIDColumnName] = "0102";
dr[MenuHelper.MenuNameColumnName] = "02 폴더";
dr[MenuHelper.MenuTypeColumnName] = MenuHelper.FolderMenuTypeCode;
dr[MenuHelper.ParentMenuIDColumnName] = "01";
dt.Rows.Add(dr);
//01드라이브/01폴더의 파일아이템
string loadUrl = "";
string deployServer = System.Configuration.ConfigurationManager.AppSettings["DeployServer"];
string smartControlsDirectory = System.Configuration.ConfigurationManager.AppSettings["SmartControlsDirectory"];
loadUrl = System.IO.Path.Combine(String.Format("http://{0}", deployServer), smartControlsDirectory);
dr = dt.NewRow();
dr[MenuHelper.MenuIDColumnName] = "010101";
dr[MenuHelper.MenuNameColumnName] = "01 화면";
dr[MenuHelper.MenuTypeColumnName] = MenuHelper.FileMenuTypeCode ;
dr[MenuHelper.FullyQualifiedTypeNameColumnName] = "BONG.WIN.CO.UserMgmt";
dr[MenuHelper.FileNameColumnName] = "BONG.WIN.CO";
dr[MenuHelper.LoadUrlColumnName] = loadUrl;
dr[MenuHelper.ParentMenuIDColumnName] = "0101";
dt.Rows.Add(dr);
dr = dt.NewRow();
dr[MenuHelper.MenuIDColumnName] = "010102";
dr[MenuHelper.MenuNameColumnName] = "02 화면";
dr[MenuHelper.MenuTypeColumnName] = MenuHelper.FileMenuTypeCode;
dr[MenuHelper.FullyQualifiedTypeNameColumnName] = "BONG.WIN.CO.UserMgmt02";
dr[MenuHelper.FileNameColumnName] = "BONG.WIN.CO";
dr[MenuHelper.LoadUrlColumnName] = loadUrl;
dr[MenuHelper.ParentMenuIDColumnName] = "0101";
dt.Rows.Add(dr);
return ds;
}
FileMenuItemInfo를 만들 때, 어셈블리 파일을 다운로드할 경로를 config에서 설정하도록 했다.
<appSettings>
<!-- 배포서버-->
<add key="DeployServer" value="dalbong2-pc" />
<!--스마트컨트롤을 다운로드할 디렉토리
최종 다운로드 경로 : "DeployServer"값 + "SmartControls Directory"값 -->
<add key="SmartControlsDirectory" value="SmartControls"/>
</appSettings>
</configuration>
이렇게 메뉴 소스를 구성한 다음 결과물인 DataSet 객체를 MenuHelper에 건네줘서 논리적인 메뉴 트리를 구성한다.
RootMenuItemInfo rootMenuInfo = MenuHelper.ConstructLogicalMenuItemInfoTree(dsMenuItemInfos);
그런 다음 넘겨 받은 RootmenuItemInfo 객체와 트리 컨트롤을 이용해서 실제로 트리 구조를 만들어낸다.
DisplayVisualTree(tvMenuTree, rootMenuInfo);
private void DisplayVisualTree(TreeView tv, RootMenuItemInfo rootMenuItemInfo)
{
// Clear the tree.
tv.Items.Clear();
//루트의 직속 자식들에 대한 treeitem을 출력한다.
TreeViewItem treeItem = null;
foreach (IMenuItemInfo itemInfo in rootMenuItemInfo.GetChildren())
{
treeItem = new TreeViewItem();
treeItem.Header = itemInfo.Name;
treeItem.Tag = itemInfo;
tv.Items.Add(treeItem);
//재귀호출을 시작한다.
ProcessItem(treeItem, itemInfo);
}
}
private void ProcessItem(TreeViewItem parentItem, IMenuItemInfo parentItemInfo)
{
TreeViewItem treeItem = null;
List<IMenuItemInfo> lstMenuItemInfos = parentItemInfo.GetChildren();
if( lstMenuItemInfos == null )
return;
foreach (IMenuItemInfo itemInfo in lstMenuItemInfos)
{
treeItem = new TreeViewItem();
treeItem.Header = itemInfo.Name;
treeItem.Tag = itemInfo;
parentItem.Items.Add(treeItem);
ProcessItem(treeItem, itemInfo);
}
}
한편 MenuHelper로부터 화면 정보 객체 목록을 건네 받아서 Dalbon2XmlObjectFactory를 이용해서 화면 정보를 등록하고 있다.
List<Dalbong2ElementInfo> _Dalbong2ElementInfos = MenuHelper.GetDalbong2ElementInfos();
_Dalbong2XmlObjectFactory = new Dalbong2XmlObjectFactory(new ConfigSectionResource("config://spring/objects"));
_Dalbong2XmlObjectFactory.RegisterDalbong2ElementDefinitions(_Dalbong2ElementInfos);
이제 이 프로그램을 실행시키면 다음과 같은 썰렁한 결과가 출력된다.
이제 좌측 최종 메뉴를 클릭하면 우측의 탭 컨트롤에 화면이 출력되는 로직을 추가할것이다. 그러나 그 전에 화면 객체를 나타내는 Dalbong2Element를 구성할 것이다. 이것도 생각이 많은 부분이다.
벌써 일요일도 다 갔다. 급한 마음으로 저녁을 먹다 뭔가 걸렸다. 체를 한 듯하다. 쓰으…소화제가 없을텐데.
'IT 살이 > 04. 기술 - 프로그래밍' 카테고리의 다른 글
개발 프레임워크 만들기 대장정 41 - 화면 객체 로딩 테스트 (0) | 2009.07.01 |
---|---|
개발 프레임워크 만들기 대장정 39 - 화면 객체 생성 (0) | 2009.06.18 |
개발 프레임워크 만들기 대장정 38 - 화면 정보 로딩 (0) | 2009.06.18 |