Dialogschritte

Ein Dialogschritt stellt technisch gesehen einen Frame dar und wird im Portal eingebettet. Er stellt damit die Hauptinteraktion mit dem Anwender dar. Ein Dialogschritt besteht aus Controls, Containern und weiterem Code.

Controls / Container sind Bestandteile von UIs

Das Layout wird mit dem WebModeler erstellt, der nach dem Paradigma What you see ist what you mean - WYSIWYM arbeitet. Der folgende Screenshot verdeutlicht dieses.

Dialogschritte werden gemäß WYSIWYM erstellt

Das Format des Dialogschritts ist XML und hat folgende Grundstruktur:

<DialogStep xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Namespace="Schleupen.CS.PI.PR.Tests.Format2x.Controls.Button"
            Name="ButtonStep" Version="1.0.0.0" Title="ButtonStep" xmlns="urn://Schleupen.CS.PI.PR.DialogStep_2.1">
  <Form>
  ...  <!-- Layout -->
  </Form>
  <Methods>
  ...
  </Methods>
  <WebserviceOperations>...</WebserviceOperations>
  <Properties>...</Properties>
  ...
</DialogStep>

Dialogschritte werden gemäß des MVVM-Musters implementiert:

Beispiel: Dialogschritt BuecherAuswaehlenStep

Das folgende Beispiel versucht das Konzept in die Umsetzung zu transportieren.

View

Der BuecherAuswaehlenStep wird in XML beschrieben:

<?xml version="1.0" encoding="utf-8"?>
<DialogStep ... Namespace="Schleupen.AS.mal.bal.bap.BuecherAusleihen" Name="BuecherAuswaehlenStep" Version="3.1.0.0" Title="Bücher auswählen">
  <Properties>
    <Property Name="ViewModel" Type="BuecherAuswaehlenStepViewModel" />
  </Properties>
  <References>
    <LibraryReference Id="Schleupen.AS.mal.bal.bap.Gateways.BuchQueryServiceClient_3.1" />
  </References>
  <CodeBehind>
    <SourceFile FileName="BuecherAuswaehlenStepStepActivity.cs" />
    <SourceFile FileName="BuecherAuswaehlenStepViewModel.cs" />
	...
  </CodeBehind>
  <Form>
    <Region Name="SearchRegion">
      <GridContainer Name="SearchCriteriaGridContainer">
	  ...
      </GridContainer>
      <StackContainer Name="SearchStackContainer" HorizontalAlignment="Right" Orientation="Horizontal">
	...
        <Button Name="Button_Search" Label="Suchen" Icon="Schleupen.CS.PI.PR.Common.Button_Search.png" Tooltip="Suchen" TabIndex="9">
          <ExecuteMethod Method="ExecuteBuecherQuery" LocksDialogStep="true" />
          ...
        </Button>
      </StackContainer>
      <Behaviours>
        <SetDefaultButton ButtonName="Button_Search" />
      </Behaviours>
    </Region>
    <DataGrid Name="BuecherSearchResultDataGrid" Label="Suchergebnisse" Items="{ViewModel.Buecher}" TabIndex="10" SearchState="{ViewModel.SearchStateResultValue}" AccessKey="">
      <Selection Selection="{ViewModel.BuecherSelection}" Mode="Multiple" />
      <Columns>
        <TextColumn Property="Titel" Label="Titel" />
        <DateTimeColumn Property="Erscheinungsdatum" Label="Erscheinungs
	...
      </Columns>
	...
    </DataGrid>
    ...
  </Form>
</DialogStep>

Hier werden die ViewModels per Data Binding deklarativ angebunden. Beispiel: Items="{ViewModel.Buecher}". Wie im Fall von Dialogabläufen wird hieraus für den gesamten Dialog die View-Code-Behind-Klasse generiert.

[System.CodeDom.Compiler.GeneratedCodeAttribute("Schleupen.CS.PI.PR.Compilation", "3.36.1.94")]
public partial class BuecherAuswaehlenStepStepActivity : Schleupen.CS.PI.PR.Engine.DialogSteps.Runtime.UsedByGeneratedCode.DialogStepDataExchange<Schleupen.AS.mal.bal.bap.BuecherAusleihen.BuecherAuswaehlenContract.V3_1.InputContract, Schleupen.AS.mal.bal.bap.BuecherAusleihen.BuecherAuswaehlenContract.V3_1.OutputContract>, Schleupen.CS.PI.PR.Engine.Validation.Runtime.UsedByGeneratedCode.ICompletionValidatable
{
	private BuecherAuswaehlenStepViewModel viewModelField;
        ...
				
	public virtual BuecherAuswaehlenStepViewModel ViewModel
	{
		get
		{
			return this.viewModelField;
		}
		set
		{
			if (viewModelField != value)
			{
				var before = this.viewModelField;
				this.viewModelField = value;
this.GetValueChangeTracker().ManageEvents(this, "ViewModel", before, value);
			}
		}
	}
	...
}

Das Main-ViewModel wird händisch per Dependency Injection erstellt!

ViewModel

Das Main-ViewModel stellt die zu bindenden ViewModels bereit:

public class BuecherAuswaehlenStepViewModel : ViewModel
{
	private readonly IBuchQueryServiceGateway buchQueryServiceGateway;

	private ViewModelCollection<BuchViewModel> buecher = new ViewModelCollection<BuchViewModel>();
	private ViewModelCollection<BuchViewModel> buecherSelection = new ViewModelCollection<BuchViewModel>();
	private ViewModelCollection<BuchViewModel> pickedBuecher = new ViewModelCollection<BuchViewModel>();
	private ViewModelCollection<BuchViewModel> pickedBuecherSelection = new ViewModelCollection<BuchViewModel>();
	private Visibility showMaxResultsWarning;
	private SearchState searchStateResultValue;
	private SearchState searchStateRecordsValue;
	private BuchCriteriaViewModel queryCriteria = new BuchCriteriaViewModel();
	private int? maxResults;
	...
	[DependsOn(PropertyPath = nameof(BuecherSelection))]
	public bool HasBuecherSelection
	{
		get { return BuecherSelection.Count > 0; }
	}
	
	public ViewModelCollection<BuchViewModel> Buecher
	{
		get { return buecher; }
		set { SetFieldValue(ref buecher, value); }
	}

	public ViewModelCollection<BuchViewModel> BuecherSelection
	{
		get { return buecherSelection; }
		set { SetFieldValue(ref buecherSelection, value); }
	}
	...
}

Namenskonvention Main-ViewModel: <Name DialogStep>ViewModel

public class BuchViewModel : ViewModel
{
	private Guid id;
	private string titel;
	private DateTime? erscheinungsdatum;
	private string isbn;
	private string sprache;
	private string autor;
	private string genre;

	public Guid Id
	{
		get { return id; }
		set { SetFieldValue(ref id, value); }
	}

	public string Titel
	{
		get { return titel; }
		set { SetFieldValue(ref titel, value); }
	}
	...
}

Namenskonvention ViewModel: <Name Entity>ViewModel

Commands als Reaktion auf einen Button-Click o.ä. werden händisch angebunden: <ExecuteMethod Method="ExecuteBuecherQuery" .../> . Diese Methode wird in einer separaten Code-Behind codiert und ist noch Teil der View. Die Methode delegiert dann direkt ins Main-ViewModel.

public partial class BuecherAuswaehlenStepStepActivity
{
	protected override void OnInitialize()
	{
		ViewModel = new BuecherAuswaehlenStepViewModel(
			IdentPatternStepInitialization.InitMaxResults(),
			new BuchQueryServiceGateway(
				this.ServiceClientFactory,
				new BuchViewModelAssembler()));
	}

	public virtual void ExecuteBuecherQuery()
	{
		ViewModel.QueryBuecher();
	}
        ...
	public class BuecherAuswaehlenStepViewModel : ViewModel
	{
		private readonly IBuchQueryServiceGateway buchQueryServiceGateway;
        ...

		public ViewModelCollection<BuchViewModel> Buecher
		{
			get { return buecher; }
			set { SetFieldValue(ref buecher, value); }
		}

		public void QueryBuecher()
		{
			Buecher.Clear();

			IEnumerable<BuchViewModel> queryResult = buchQueryServiceGateway.Query(QueryCriteria, MaxResults).ToArray();
			Buecher.AddRange(queryResult);
			...
		}
	}

Die Implementierung der angeschlossenen Klassen Gateway und Assembler erfolgt identisch zu den Kontexten der Serviceimplementierung eines Landes, der Workflows usw.

public class BuchQueryServiceGateway : IBuchQueryServiceGateway
{
	private readonly IBuchViewModelAssembler buchViewModelAssembler;
	private readonly IServiceClientFactory serviceClientFactory;
	...
	public IEnumerable<BuchViewModel> Query(BuchCriteriaViewModel buchCriteriaViewModel, int? maxResults)
	{
		var andRelation = new AndRelationContract();
		andRelation.BuchCriteria = buchViewModelAssembler.ToDataContract(buchCriteriaViewModel);

		var queryRequest = new QueryRequest();
		queryRequest.Restriction = andRelation;
		...

		using (var serviceClient = serviceClientFactory.CreateClient<IBuchQueryServiceClient>())
		{
			QueryResponse queryResponse = serviceClient.Query(queryRequest);

			return buchViewModelAssembler.ToViewModel(queryResponse.BuchInfoListe);
		}
	}
}

Namenskonvention Gateway: <Servicename>Gateway

Der Assembler wird straight-forward implementiert:

public class BuchViewModelAssembler : IBuchViewModelAssembler
{
	public IEnumerable<BuchViewModel> ToViewModel(IEnumerable<BuchInfoContract> buchInfoContracts)
	{
		if (buchInfoContracts == null) { return Array.Empty<BuchViewModel>(); }

		var buchViewModels = new List<BuchViewModel>();
		foreach (BuchInfoContract buchInfoContract in buchInfoContracts)
		{
			BuchViewModel buchViewModel = ToViewModel(buchInfoContract);
			if (buchViewModel != null)
			{
				buchViewModels.Add(buchViewModel);
			}
		}

		return buchViewModels.ToArray();
	}

	private BuchViewModel ToViewModel(BuchInfoContract buchInfoContract)
	{
		if (buchInfoContract == null) { return null; }

		return new BuchViewModel
			{
				Autor = buchInfoContract.Autor,
				Titel = buchInfoContract.Titel,
				...	
			};
	}
}

Namenskonvention Assembler: <Name ViewModel>Gateway

Model

Das Model wird in einer Library (siehe unten) erstellt. Diese beinhaltet nur die Referenz auf den Service und kann somit wiederverwendet werden.

Namenskonvention Gateway: <Servicename>Client

Komplexe Dialogschritte

Haben die Dialoge eine höhere Komplexität, so kann die Unterteilung in weitere ViewModels sinnvoll sein. Das folgende Diagramm veranschaulicht die Strategie:

Dabei werden Bereiche wie beispielsweise der Suchbereich als eigenes ViewModel ausgeprägt und hier entsprechend der Service zum Suchen angebunden.

Eine weitere Möglichkeit ist es, fachliche Logik hinter sogenannten Composite Controls zu kapseln und bereitzustellen. Composite Controls stellen eine fachliche Funktionalität wie das Einsehen von Aufgaben-Details dar, die als nuget-Package bereitgestellt werden und der Entwickler entsprechend per nuget einbindet. Prinzipiell ist es dabei egal, nach welchem Paradigma dieses Composite Control erstellt worden ist, aber auch hier bietet sich natürlich MVVM an. Die Einbindung dieses Controls erfolgt dann im XML wie folgt:

 <References>
    <CompositeControlReference Include="Schleupen.CS.PI.PR.CompositeControls.MyCompositeControl" As="MyCompositeControl" />
  </References>
...
<Form> 
    <CompositeControl Name="MyControl" Type="MyCompositeControl" DataContext="{MyRootViewModel}" TabIndex="10" />
  </Form>
Cookie Consent mit Real Cookie Banner