public static SaveResult save(PreTransactionAction preTransactionAction, preConcDelegate pcDelegate, PreConcArguments pcArguments, preSaveDelegate psDelegate, PreSaveArguments psArguments, PostSaveAction postSaveAction, bool activateOpcaoCancelar)
		{
			Concorrencia conc = new Concorrencia();
			IDbTransaction tran = null;
            SaveResult successfulSave = SaveResult.successful;
			GisaDataSetHelper.HoldOpen ho = null;
			long startTicks = 0;
			bool savedWithoutDeadlock = false;
			// Variavel que indica qual a tabela que está a ser gravada (tem o debugging como fim)
			string currentTable = string.Empty;
			DataSet gBackup = null;
			ArrayList changedRowsArrayList = null;
			// Variável que vai manter a informação das linhas "added" cujos Ids são gerados automaticamente
			// antes e depois de serem gravadas (antes de essas linhas serem gravadas os seus IDs são
			// negativos e depois são-lhe atribuidos valores positivos
			Hashtable trackNewIds = new Hashtable();

            try
            {
                if (preTransactionAction != null)
                {
                    preTransactionAction.ExecuteAction();
                    if (preTransactionAction.args.cancelAction)
                    {
                        MessageBox.Show(preTransactionAction.args.message, "Erro", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                        GisaDataSetHelper.GetInstance().RejectChanges();
                        return SaveResult.nothingToSave;
                    }
                }
            }
            catch (Exception e) { 
                Trace.WriteLine(e.ToString());
                return SaveResult.unsuccessful; }
            finally { }



			while (! savedWithoutDeadlock)
			{
				try
				{
					if (pcDelegate != null)
					{
						ho = new GisaDataSetHelper.HoldOpen(GisaDataSetHelper.GetConnection());
						tran = ho.Connection.BeginTransaction(GisaDataSetHelper.GetTransactionIsolationLevel());
						pcArguments.tran = tran;
						gBackup = new DataSet();
						gBackup.CaseSensitive = true;
						pcArguments.gisaBackup = gBackup;
						pcArguments.continueSave = true;
						pcDelegate(pcArguments);

						//no caso de se pretender apagar uma relacao hierarquica ha a possibilidade de 
						//não ser necessário prosseguir com a gravação dos dados por razoes de logica
						//(ver o delegate verifyIfCanDeleteRH)
						if (! pcArguments.continueSave)
						{
							tran.Commit();
							savedWithoutDeadlock = true;
                            successfulSave = SaveResult.unsuccessful;
                            break;
						}
					}

					//verifica logo à cabeça se houve de facto alguma alteração ao dataset
					if (! (GisaDataSetHelper.GetInstance().HasChanges()))
					{
                        return SaveResult.nothingToSave;
					}

					//obter um arraylist com estruturas que indicam para cada tabela quais as linhas que foram alteradas
					changedRowsArrayList = getCurrentDatasetChanges(conc);
					if (changedRowsArrayList == null)
					{
                        return SaveResult.nothingToSave;
					}

					while (true)
					{
						if (ho == null)
						{
							ho = new GisaDataSetHelper.HoldOpen(GisaDataSetHelper.GetConnection());
						}
						if (tran == null)
						{
							tran = ho.Connection.BeginTransaction(GisaDataSetHelper.GetTransactionIsolationLevel());
						}

						//manter 2 datasets com linhas provenientes da bd
						//isto para permitir a detecção de novos e possiveis conflitos qd o utilizador perante 1 problema de concorrencia pretende manter as suas alterações
						//a estratégia passa por verificar se houve alterações no 1º (e principal) dataset com as linhas da bd
						if (originalRowsDB1 == null)
						{
							//guardar as linhas neste dataset

							//FIXME: changedRowsArrayList esta a chegar com tabelas repetidas (!)
							originalRowsDB1 = conc.getOriginalRowsDB(changedRowsArrayList, tran);
						}
						else if (originalRowsDB1 != null && originalRowsDB2 == null)
						{
							//o dataset principal (originalRowsDB1) ja esta preenchido e o utilizador pretende manter as suas alterações

							//o dataset principal (originalRowsDB1) ja esta preenchido e o utilizador pretende manter as suas alterações
							// as linhas da BD vao parar ao 2º dataset
							originalRowsDB2 = conc.getOriginalRowsDB(changedRowsArrayList, tran);



							if (originalRowsDB2 == null)
							{
								originalRowsDB1 = null;
							}
						}
						else if (originalRowsDB1 != null && originalRowsDB2 != null)
						{
							//os 2 datasets estao preenchidos (o que quer dizer que estamos perante a 2ª situação de concorrencia consecutiva (no mínimo) na mesma mudança de contexto)

							//nesta situação o dataset principal passa a ter as alterações contidas no 2º para que neste último passem a constar os novos dados provenientes da BD
							//se não existirem novas linhas, quer dizer que as alterações feitas em memória coincidem com aquelas existentes na BD

							//verificar se existem novas linhas
							if (! (conc.getOriginalRowsDB(changedRowsArrayList, tran) == null))
							{
								originalRowsDB1 = originalRowsDB2.Copy();
								originalRowsDB2 = conc.getOriginalRowsDB(changedRowsArrayList, tran);
							}
							else
							{
								originalRowsDB1 = null;
								originalRowsDB2 = null;
							}
						}

						//se não existirem conflitos de concorrencia
						if (! ((originalRowsDB1 != null && originalRowsDB2 == null && conc.temLinhas(originalRowsDB1)) || (originalRowsDB1 != null && originalRowsDB2 != null && conc.wasModified(originalRowsDB1, originalRowsDB2, changedRowsArrayList))))
						{
							//não há concorrência quando o originalRowsDB1 não tem linhas e originalRowsDB2 está vazio ou quando os dois datasets são diferentes

							break;
						}
						else
						{
							//caso existam conflitos, é apresentado ao utilizador uma mensagem a indicar os pontos de conflito e de que forma os pretende resolver


							//ToDo: verificar a necessidade de fazer rollback
							tran.Rollback();
							ho.Dispose();
							ho = null;
							tran = null;

							frm.DetalhesUser = Concorrencia.StrConcorrenciaUser.ToString();
							frm.DetalhesBD = Concorrencia.StrConcorrenciaBD.ToString();
							frm.btnCancel.Enabled = activateOpcaoCancelar;

							switch (frm.ShowDialog())
							{
								case DialogResult.Yes:
									//mantem-se dentro do ciclo de forma a voltar a verificar se existe concorrencia ou nao
									//ao mesmo tempo que este tratamento de conflitos e executado outro utilizador pode já ter feito novas alterações                           

									//é necessario limpar as variaveis que mantem as mensagens sobre a concorrencia para o caso de neste situação ainda existirem novos situações de conflito
									Concorrencia.StrConcorrenciaBD.Remove(0, Concorrencia.StrConcorrenciaBD.Length);
									Concorrencia.StrConcorrenciaUser.Remove(0, Concorrencia.StrConcorrenciaUser.Length);

									break;
								case DialogResult.No:
									//gravar em memoria as linhas obtidas da base de dados e sai do metodo
									if (originalRowsDB2 != null)
									{
										//se no dataset originalRowsDB2 existirem linhas, logo é este que vai ser gravado
										conc.MergeDatasets(originalRowsDB2, GisaDataSetHelper.GetInstance(), DataSetTablesOrderedA);
									}
									else
									{
                                        conc.MergeDatasets(originalRowsDB1, GisaDataSetHelper.GetInstance(), DataSetTablesOrderedA);
									}
									conc.ClearRowsChangedToModified();
									cleanConcurrencyVariables();

									return successfulSave;
								case DialogResult.Cancel:
									cleanConcurrencyVariables();

                                    successfulSave = SaveResult.cancel;
									return successfulSave;
							}
						}
					}

					startTicks = DateTime.Now.Ticks;

					if (Concorrencia.StrConcorrenciaLinhasNaoGravadas.Length > 0)
					{
						MessageBox.Show("A informação referente aos campos seguintes não pode ser gravada " + Environment.NewLine + "por ter sido, entretanto, eliminada: " + System.Environment.NewLine + Concorrencia.StrConcorrenciaLinhasNaoGravadas.ToString(), "Gravação de dados.");
					}

					// Chamar qualquer tarefa de "pré-gravação" que possa ter sido definida
					if (psDelegate != null)
					{
						psArguments.tran = tran;
						psDelegate(psArguments);
					}

					// forma de manter a informação referente à actualização dos Ids das linhas quando 
					// estas são adicionadas na base de dados, isto é, saber qual o valor (negativo) 
					// do ID antes da linha ser gravada e o valor (positivo) atribuído pela base de dados
					// depois do save
					trackNewIds.Clear();
					conc.startTrackingIdsAddedRows(GisaDataSetHelper.GetInstance(), changedRowsArrayList, ref trackNewIds);

					// garantir que filhos das linhas a eliminar que não estão carregados ficam também eles eliminados
					//ToDo: em getChildRowsFromDB utilizar dataset de trabalho para obter as relações entre tabelas. Passar ao metodo o origRowsDB para que as linhas obtidas lhe sejam directamente adicionadas
					ArrayList rows = new ArrayList();
					ArrayList afectedTables = new ArrayList();
					GisaDataSetHelper.ManageDatasetConstraints(false);
					foreach (Concorrencia.changedRows changedRow in changedRowsArrayList)
					{
						currentTable = changedRow.tab;
						if (changedRow.rowsDel.Count > 0)
						{
							cascadeManageChildDeletedRows(changedRow.tab, changedRow.rowsDel, conc, tran);
						}

						// alterar o estado das Rowstate.Deleted para Rowstate.Unchanged e passa-las a isDeleted=True
						DataRow delRow = null;

						if (changedRow.rowsDel.Count > 0)
						{

							while (changedRow.rowsDel.Count > 0)
							{
                                if (changedRow.tab.Equals("ControloAutDataDeDescricao") || changedRow.tab.Equals("FRDBaseDataDeDescricao"))
                                {
                                    delRow = (DataRow)(changedRow.rowsDel[0]);
                                    delRow.RejectChanges();
                                    changedRow.rowsDel.RemoveAt(0);
                                }
                                else
                                {
                                    delRow = (DataRow)(changedRow.rowsDel[0]);
                                    delRow.RejectChanges();
                                    delRow["isDeleted"] = 1;
                                    changedRow.rowsMod.Add(delRow);
                                    changedRow.rowsDel.RemoveAt(0);
                                }
							}
						}

						rows.Clear();
						rows.AddRange(changedRow.rowsAdd);
						rows.AddRange(changedRow.rowsMod);
						PersistencyHelperRule.Current.saveRows(GisaDataSetHelper.GetInstance().Tables[changedRow.tab], (DataRow[])(rows.ToArray(typeof(DataRow))), tran);
					}

                    if (postSaveAction != null)
                    {
                        postSaveAction.args.tran = tran;
                        postSaveAction.ExecuteAction();
                    }

					conc.ClearRowsChangedToModified();
					GisaDataSetHelper.ManageDatasetConstraints(true);
					tran.Commit();
					savedWithoutDeadlock = true;
					Debug.WriteLine("Save: " + new TimeSpan(DateTime.Now.Ticks - startTicks).ToString());
					Trace.WriteLine("Save completed.");
				}
				catch (Exception ex)
				{
                    Trace.WriteLine(ex);

					//GisaDataSetHelper.GetInstance().RejectChanges()
					if (DBAbstractDataLayer.DataAccessRules.ExceptionHelper.isDeadlockException(ex))
					{
						Trace.WriteLine(">>> Deadlock (save).");
						conc.prepareRollBackDataSet(ref trackNewIds);
						tran.Rollback();
						tran = null;
						if (conc.mGisaBackup != null && gBackup != null)
						{
                            conc.MergeDatasets(gBackup, conc.gisabackup, DataSetTablesOrderedA);
						}
						if (conc.mGisaBackup != null)
						{
                            conc.MergeDatasets(conc.mGisaBackup, GisaDataSetHelper.GetInstance(), DataSetTablesOrderedA, trackNewIds);
						}
						conc.deleteUnusedRows(GisaDataSetHelper.GetInstance(), ref trackNewIds);
						gBackup = null;
					}
					else if (DBAbstractDataLayer.DataAccessRules.ExceptionHelper.isTimeoutException(ex))
					{
						Trace.WriteLine(">>> Timeout (save).");
						conc.prepareRollBackDataSet(ref trackNewIds);
						tran = null;
						if (conc.mGisaBackup != null && gBackup != null)
						{
                            conc.MergeDatasets(gBackup, conc.gisabackup, DataSetTablesOrderedA);
						}
						if (conc.mGisaBackup != null)
						{
                            conc.MergeDatasets(conc.mGisaBackup, GisaDataSetHelper.GetInstance(), DataSetTablesOrderedA, trackNewIds);
						}
						conc.deleteUnusedRows(GisaDataSetHelper.GetInstance(), ref trackNewIds);
						gBackup = null;
					}
					else
					{
	#if DEBUG
						Trace.WriteLine(currentTable);
	#endif
						Trace.WriteLine("Save failed.");
						Trace.WriteLine(ex);
						Debug.Assert(false, "Save failed.");

						if (tran != null)
						{
							tran.Rollback();
						}
						tran = null;

						MessageBox.Show("Ocorreu um erro inesperado durante a gravação " + Environment.NewLine + "da informação e por esse motivo a aplicação irá " + Environment.NewLine + "fechar. Por favor contacte o administrador do sistema", "Erro", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        throw;
					}
				}
				finally
				{
					if (tran != null)
					{
						tran.Dispose();
					}

					cleanConcurrencyVariables();

					if (savedWithoutDeadlock)
					{
						conc.mGisaBackup = null;
					}

					if (ho != null)
					{
						ho.Dispose();
						ho = null;
					}
				}
			}


			return successfulSave;
		}