22
Iul
2010

Acest articol reprezinta o continuare a articolului de serializare a datelor in aplicati dialog folosind MFC. In acel articol am prezentat o modalitate de rezolvare a conflictelor generate de versiuni diferite de fisiere specifice unei aplicati, bazandu-ne pe serializarea oferita de MFC. Totusi, arhitectura aplicatiilor de tip dialog nu e ceea mai potrivita si des folosita pentru aplicatiile in care e nevoie sa folosim serializarea.

Aplicatiile bazate pe arhitectura document-interfata sunt cele care se preteaza cel mai bine pentru ideea de reprezentare de documente multiple, cum este cazul aplicatiilor Microsoft Office. Arhitectura doc/view ofera suport pentru salvarea si incarcarea automata a documentelor in/din fisiere pe disc printr-un mecanism numit serialiazare. Aplicatiile MDI si SDI (Single Document Interface) ofera implicit acest schelet pentru serializare.

Screen shot cu aplicatia
Vezi imaginea la dimenisunea normala

Serializarea este customizabila. Important este sa alegi formatul binar al elementelor si numarul acestora iar apoi sa completezi metoda de serializare. Intr-o aplicatie document-view metodele clasei document CDocument sunt mapate pe optiunile New, Open, Save si Save As... disponibile in meniul File. Aceste functii membre se ocupa de creearea si deschiderea obiectului, trackingul modificarilor de status din document si serializeaza datele in fisier.

Aplicatiile MDI creeaza cate o instanta a clasei derivate din CDocument pentru fiecare document, in timp ce aplicatiile SDI reutilizeaza singura instanta derivate din CDocument pentru fiecare fisier deschis. Clasa CDocument si derivatele sale sunt responsabile cu controlul serializarii tuturor datelor continute de ea. Ea contine notite despre modificarile efectuate asupra documentului astfel incat programul sa poata atentiona daca se doreste inchiderea aplicatiei si documentul nu e salvat cu ultimele modificari. Cand un document este incarcat, se creeaza o instant a clasei CArhive pentru citire si arhiva este deserializata din document. Cand un document este salvat, se creeaza o instanta pentru scriere si documentul este scris in arhiva.

Rutinele clasei CArhive utilizata pentru citire si scriere in fisierul de stocare sunt optimizate pentru a furniza performanta chiar si daca se serializeaza multe obiecte mici.

Revenind la aplicatia demo, am utilizat aceeasi idee de aplicatie cu cartea de adrese folosita la articolul mai sus mentinat. Avem tot doua versiuni de fisiere.

Difera modul de utilizare a serializari cat si interactiunea intre clasele proiectului. Locul clasei CAdressBook este luat de clasa document CSerAddressBookMDIDoc.

Intr-o aplicatie reala este recomandata folosirea unui element unic de identificare (UID) pentru a "pivota" (cauta pentru identificarea obiectelor). Pentru simplitatea aplicatiei demo, elementul dupa care caut este primul nume si nu poate fi modificat.

Clasa Document - CSerAddressBookMDIDoc

Interfata clasei document este listata mai jos.

class CSerAddressBookMDIDoc : public CDocument
{
protected: // create from serialization only
	CSerAddressBookMDIDoc();
	DECLARE_DYNCREATE(CSerAddressBookMDIDoc)

// Attributes
public:

// Operations
public:

// Overrides
public:
	virtual BOOL OnNewDocument();
	virtual void Serialize(CArchive& ar);

	void SetFileVersion(UINT nFV)	{	m_nFileVersionSchema = nFV;	}
	UINT GetFileVersion() const	{	return m_nFileVersionSchema;	}

	const		ContactList& GetContacts() const {return m_cContactsList;}
	bool		AddContact(const Contact& contact);
	bool		RemoveContact(const CString& firstname, const CString& lastname);
	POSITION	FindContact(const CString& firstname) const;
	bool		FindContact(const CString& firstname, Contact& contact) const;
	bool		UpdateContact(const CString& firstname, Contact& contact);

// Implementation
public:
	virtual ~CSerAddressBookMDIDoc();
	BOOL DoSave(LPCTSTR lpszPathName, BOOL bReplace);  // need to override this
	virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);

#ifdef _DEBUG
	virtual void AssertValid() const;
	virtual void Dump(CDumpContext& dc) const;
#endif

	ContactList m_cContactsList;

// Generated message map functions
protected:
	DECLARE_MESSAGE_MAP()

private:
	INT m_nFileVersionSchema;
};

Dupa cum se poate observa, de aceasta data se foloseste macroul DECLARE_DYNCREATE(). Acest macro permite creearea dinamica a obiectelor document la run time (cazul aplicatiilor MDI).

Am refolosit metodele din clasa CAdressBook ce manipuleaza obiectele din lista m_cContactsList. ContactList este un alias pentru lista de contacte:

typedef CList<Contact, Contact>		ContactList;

Metoda de serializare a datelor in clasa document este listata mai jos:

void CSerAddressBookMDIDoc::Serialize(CArchive& ar)
{
	if (ar.IsStoring())			// storing
	{
		ar << m_nFileVersionSchema;

		// write the number of contacts
		ar << (int)m_cContactsList.GetCount();
		Contact::SetCurrentContactVersion(m_nFileVersionSchema);

		// write all the contacts
		POSITION pos = m_cContactsList.GetHeadPosition();
		while(pos != NULL)
		{
			Contact contact = m_cContactsList.GetNext(pos);

			contact.Serialize(ar);
		}
	}
	else						// loading
	{
		ar >> m_nFileVersionSchema;

		m_cContactsList.RemoveAll();

		int count = 0;
		ar >> count;

		// read the number of contacts
		while (count-- > 0)
		{
			Contact contact;
			contact.Serialize(ar);

			m_cContactsList.AddTail(contact);
		}
	}
	
	UpdateAllViews(NULL);
}

Aceasta metoda citeste sau scrie referinta clasei serializate (Contact) in obiectul CArhive la run time. Pe ramura de TRUE a if-ului se salveaza informatiile din lista in fisier, iar pe ramura FALSE se incarca datele din fisier in lista. Pentru stocare, initial, se salveaza versiunea fisierului (m_nFileVersionSchema) si numarul de elemente. Apoi se intereaza peste toate elementele de tip Contact si se serializeaza pentru stocare informatiile. Pentru incarcare, se citeste versiunea fisierului, se curate lista, se ia numarul de elemente de tip Contact stocate in fisier, si cat timp numarul numarul de elemente e pozitiv se vor deserializa informatiile de contact din fisier. Toate entintatile de tip contact deserializate ajung in lista m_cContactsList. Peste aceasta lista se va itera pentru afisarea informatiilor existente in fisierele incarcate.

Clasa interna serializabila - Contact

Dupa cum se poate observa, in metoda de serializare din clasa CSerAddressBookMDIDoc, in ambele situatii (incarcare/stocare) pentru fiecare obiect se creeaza cate o instant a clasei Contact in ideea de-a trimite/citii efectiv datele la mediul de stocare. Metoda de serializare a entitatilor ce compun un contact este implementata in clasa Contact:

void Contact::Serialize( CArchive& ar )
{
	if (ar.IsStoring())
	{
		CRuntimeClass* pruntime = Contact::GetRuntimeClass();
		int oldnr = pruntime->m_wSchema;
		pruntime->m_wSchema = CURRENT_VERSION;

		ar.SerializeClass(pruntime);

		switch (CURRENT_VERSION)
		{
		case 1:
			ar << m_strFirstName << m_strLastName << m_strAddress << m_strPhone;
			break;

		case 2:
			ar << m_strFirstName << m_strLastName << m_strAddress << m_strPhone << m_strMobilePhone << m_strEmail;
			break;

		default:
			// unknown version for this object
			AfxMessageBox(_T("Unknown file version."), MB_ICONSTOP);
			break;
		}

		pruntime->m_wSchema = oldnr;
	}
	else      // loading code
	{
		ar.SerializeClass(RUNTIME_CLASS(Contact));

		UINT nVersion = ar.GetObjectSchema();

		switch (nVersion)
		{
		case 1:
			ar >> m_strFirstName >> m_strLastName >> m_strAddress >> m_strPhone;
			m_strMobilePhone = _T("");
			m_strEmail = _T("");
			break;

		case 2:
			ar >> m_strFirstName >> m_strLastName >> m_strAddress >> m_strPhone >> m_strMobilePhone >> m_strEmail;
			break;

		default:
			// unknown version for this object
			AfxThrowArchiveException(CArchiveException::badSchema);
			break;
		}
	}
}

Pentru stocare, initial obtin un pointer la runtime pentru clasa serializata, in ideea de-a seta versiunea de schema dorita. Apoi, in functie de numarul versiunii fac serializarea si la sfarsit restabilesc valoarea initiala a valorii schemei clasei serializate.

Pentru incarcare apelez metoda de serializare a clasei Contact, iau versiunea salvata din fisier si apoi in functie de ea aduc in clasa document informatiile furnizate de instant CArhive (ar).

Clasa reprezentare - CSerAddressBookMDIView

Aceasta clasa este responsabila cu reprezentarea grafica a continutului documentelor. Pentru aceast exemplu, ea este derivate din CListView si are setat stilul REPORT pentru a reprezenta datele ca intr-un grid.

Metoda PopulateList() este resposabila cu afisarea datelor documentului in lista noastra.

void CSerAddressBookMDIView::PopulateList()
{
	CSerAddressBookMDIDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);

	CreateViews(pDoc->GetFileVersion());

	CListCtrl	*pListCtrl = &GetListCtrl();
	ASSERT_VALID(pListCtrl);

	// get a reference to the contacts list
	const ContactList& contacts = pDoc->GetContacts();

	// iterate over all contacts add add them to the list
	int nCurrentItem = 0;
	POSITION pos = contacts.GetHeadPosition();
	while(pos != NULL)
	{
		const Contact contact = contacts.GetNext(pos);

		nCurrentItem = pListCtrl->InsertItem(nCurrentItem, contact.GetFirstName());
		pListCtrl->SetItemText(nCurrentItem, 1, contact.GetLastName());
		pListCtrl->SetItemText(nCurrentItem, 2, contact.GetAddress());
		pListCtrl->SetItemText(nCurrentItem, 3, contact.GetPhoneNumber());

		switch(pDoc->GetFileVersion())
		{
		case 1:
			break;
		case 2:
			pListCtrl->SetItemText(nCurrentItem, 4, contact.GetMobileNumber());
			pListCtrl->SetItemText(nCurrentItem, 5, contact.GetEmail());
			break;
		default:
			break;
		}
	}
}

Prima data obtinem pointerul la documentul curent. Apoi, apelam metoda CreateViews(), metoda responsabila cu inserarea coloanelor in functie de versiunea fisierului. Obtinem un pointer la lista din CListView si apoi o referinta la primul contact din lista de contacte. Apoi cat timp avem elemente, iteram intr-un while() peste elementele listei si inseram informatiile in controlul list.

Metoda PopulateList() este apelata in metoda suprascrisa OnUpdate() din CSerAddressBookMDIView, metoda ce e apelata de frameworkul MFC de fiecare data cand un document este modificat. Metoda OnUpdate() e apelata din CDocument::UpdateAllViews() si are implementarea de baza in clasa CView.

Pentru a putea adauga/sterge/modifica inregistrari din/in documente am creeat un dialog special, apelat din meniul my Menu. Metoda de afisare a dialogului modal este prezentata mai jos.

void CSerAddressBookMDIView::OnMymenuChangedata()
{
	CSerAddressBookMDIDoc *pDoc = GetDocument();
	ASSERT_VALID(pDoc);

	CManipulateDataDlg dlg;

	dlg.SetAddressDocument(pDoc);

	if (IDOK == dlg.DoModal())
	{
		PopulateList();
	}
}

Pentru ca trebuie sa interactionez din dialogul meu cu lista de contacte din documentul curent, sunt nevoit sa pasez pointerul la clasa document ( dlg.SetAddressDocument(pDoc) ) clasei dialog de manipulare a datelor. Apoi, fac dialogul modal. La inchiderea dialogului, daca s-a selectat butonul OK, atunci reprezentarea datelor trebuie actualitata si de aceea apelez din nou PopulateList().

Clasa CManipulateDataDlg

Aceasta clasa este responsabila cu manipularea informatiilor din lista de contacte a documentului curent. Diferenta fata de dialogul din articolul precedent e in ea nu se realizeaza partea de incarcare si salvare a datelor. Acest rol e preluat de clasele arhitecturii document-view.

Metoda de populare a controlului lista din aceast dialog este:

void CManipulateDataDlg::PopulateList()
{
	// delete all current members
	m_cList.DeleteAllItems();

	// get a reference to the contacts list
	const ContactList& contacts = m_pAddressDoc->GetContacts();

	// iterate over all contacts add add them to the list
	int nCurrentItem = 0;
	POSITION pos = contacts.GetHeadPosition();
	while(pos != NULL)
	{
		const Contact contact = contacts.GetNext(pos);

		nCurrentItem = m_cList.InsertItem(nCurrentItem, contact.GetFirstName());
		m_cList.SetItemText(nCurrentItem, 1, contact.GetLastName());
		m_cList.SetItemText(nCurrentItem, 2, contact.GetAddress());
		m_cList.SetItemText(nCurrentItem, 3, contact.GetPhoneNumber());

		
		switch(m_pAddressDoc->GetFileVersion())
		{
		case 1:
			break;
		case 2:
			m_cList.SetItemText(nCurrentItem, 4, contact.GetMobileNumber());
			m_cList.SetItemText(nCurrentItem, 5, contact.GetEmail());
			break;
		}
	}
}

De fiecare data se curata lista si se obtine o referinta la lista de obiecte a documentului. Daca exista un document incarcat de versiune 1 sau 2 atunci controalele dialogului sunt particularizate. Apoi se itereaza peste elementele listei de tip ContractList si se insereaza informatiile in controlul lista.

Suport pentru mai multe extensii de fisiere in MDI

Aplicatiile MDI vin implicit cu suport doar pentru un anumit tip de fisier, implicit, un unic tip de extensie. Adeseori, e nevoie ca aplicatia noastra sa suporte mai multe tipuri de fisiere si mai multe extensii. In cazul nostru, este nevoie ca aplicatia sa suporte complet atat fisierele din versiunea 1 (*.sab1), cat si fisierele din versiunea 2 (*.sab2). De asemenea, trebuie sa putem converti dintr-o versiune veche in una noua si reciproca.

Informatii detaliate despre cum se adauga suport pentru mai multe fisiere in aplicatii document-view in MFC se gasesc la adresa http://support.microsoft.com/kb/141921. O alta referinta utila se gaseste aici.

Pornind de la aceste referinte, am facut ca aplicatia mea sa suporte ambele tipuri de fisiere. Merita enumerate modificarile din metoda din initializare a aplicatiei, InitInstance din CSerAddressBookMDIApp.

BOOL CSerAddressBookMDIApp::InitInstance()
{
	// ---------- 
	// MFC's wizard generated code…
      // ----------
	SetRegistryKey(_T("Local AppWizard-Generated Applications"));
	LoadStdProfileSettings(4);  // Load standard INI file options (including MRU)
	// Register the application's document templates.  Document templates
	//  serve as the connection between documents, frame windows and views

	m_pDocManager = new CMultiDocManager;  // Silviu

	CMultiDocTemplate* pDocTemplate;
	pDocTemplate = new CMultiDocTemplate(IDR_SerAddressBookTYPE,
		RUNTIME_CLASS(CSerAddressBookMDIDoc),
		RUNTIME_CLASS(CChildFrame), // custom MDI child frame
		RUNTIME_CLASS(CSerAddressBookMDIView));
	if (!pDocTemplate)
		return FALSE;
	AddDocTemplate(pDocTemplate);

	//Silviu
	pDocTemplate = new CMultiDocTemplate(
		IDR_SerAddressBook2TYPE,
		RUNTIME_CLASS(CSerAddressBookMDIDoc),
		RUNTIME_CLASS(CChildFrame),				// custom MDI child frame
		RUNTIME_CLASS(CSerAddressBookMDIView));
	if (!pDocTemplate)
		return FALSE;
	AddDocTemplate(pDocTemplate);

	//Silviu
	pDocTemplate = new CMultiDocTemplate(
		IDR_SerAddressBook3TYPE,
		RUNTIME_CLASS(CSerAddressBookMDIDoc),
		RUNTIME_CLASS(CChildFrame),				// custom MDI child frame
		RUNTIME_CLASS(CSerAddressBookMDIView));
	if (!pDocTemplate)
		return FALSE;
	AddDocTemplate(pDocTemplate);


	// create main MDI Frame window
	CMainFrame* pMainFrame = new CMainFrame;
	if (!pMainFrame || !pMainFrame->LoadFrame(IDR_SerAddressBookTYPE))
	{
		delete pMainFrame;
		return FALSE;
	}

      // ---------- 
	// MFC's wizard generated code…
      // ----------

	return TRUE;
}

Primul aspect care iese in evidenta, dupa apelul LoadStdProfileSettings() (functie scrisa de wizard-ul MFC), este apelul initializarea atributului m_pDocManager (pointer la CDocManager folosit pentru managementul template-urilor de document) cu un pointer la un nou obiect CMultiDocManager. Clasa CMultiDocManager suprascrie metodele CreateNewDocument(), DoPromptFileName(), OnFileNew() din CDocManager.

Apoi, pe langa template-ul de document default al aplicatiei (cu id-ul de resursa IDR_SerAddressBookTYPE), mai creez doua template-uri pentru cele doua tipuri de documente. Toate aceste template-uri de documente sunt adaugate (AddDocTemplate()) in lista de template-uri document disponibile ale aplicatiei. Ultima modificare semnificativa din InitInstance() implica incarcarea ferestrei cadru corespunzatoare meniului din resurse, dorita (IDR_SerAddressBookTYPE - contine optiunile de Save si Save As).

Concluzie

Arhitectura Multiple Document Interface (MDI) se preteaza cel mai bine pentru aplicati de tip container de date. Framework-ul MFC ofera un suport stabil si complet pentru manipularea obiectelor serializabile, suportul incluzand atat stocarea cat si reprezentarea. O parte din aplicatiile suitei Microsoft Office sunt construite folosind aceasta arhitectura.