Eigene Messagebox erstellen im MVVM-Pattern

  • WPF MVVM
  • .NET 5–6

Es gibt 86 Antworten in diesem Thema. Der letzte Beitrag () ist von kafffee.

    kafffee schrieb:

    Du meinst aber, wenn MainWindow das Dialogfester sein soll, oder?

    Sowohl als auch.

    Alles spielt sich in dem View-Projekt ab. Dort sind deine UserControls drinnen.
    Im MainWindow ist NUR ein ContentControl.

    kafffee schrieb:

    In meinem Fall jetzt soll ja MainWindow nur zu Testzwecken dienen und den Button anzeigen, auf dessen Klick dann das Dialogfenster geöffnet wird...

    Diese Buttons hast du ein einem UserControl. Per DataTemplate wird bestimmt das dieses UserControl gerendert wird wenn dein "MainViewModel" als Datencontext übergeben wird.

    Das App-Projekt ist wirklich sauber und so leer als möglich zu halten.

    Wenn du in deinem MainWindow zwei Buttons hast dann hast du die ja immer drinnen. Das MainWindow ist das Window was dein "Hauptfenster" deiner Anwendung darstellen soll.

    Was deinen Code betrifft ist alles gut. Das sollte so funzen. Ansonsten lade mal das Projekt hoch, mal sehen was da quer steht. Bindingfehler im XAML Bindingfenster?

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Nofear23m schrieb:

    Diese Buttons hast du ein einem UserControl. Per DataTemplate wird bestimmt das dieses UserControl gerendert wird wenn dein "MainViewModel" als Datencontext übergeben wird.

    Genau das hab ich soweit verstanden, das Thema hatten wir ja schon bei meinem selbstgebauten TabControl, du erninnerst dich?

    Ich hab jetzt in meinem MainWindow keinen ContentPresenter drinnen sondern halt ein UserControl MainView, weil ich die Inhalte da nicht wechseln muss. Ich dachte das macht nur Sinn, wenn ich in meinem MainWindow die Ansichten wechseln will...

    Anbei mal das Projekt zwecks dem BindingFehler

    Bitte schau auch nochmal meinen Post #20 an, ich hab noch zwei Fragen ergänzt.
    Dateien
    Nachdem ich den DataContext von MainView ins MainWindow der App-Assembly verschoben hatte, funktionierte es.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    Da haben wir es ja.

    Ordnung halten!

    Du hast in deinem App-Projekt ein Fenster (MainWindow). In diesem ist diesem Button.
    Dann hast du aber ein MainView UserControl in deinem View-Projekt mit auch genau einem gleich aussehenden Button. DU dachtest das beim Start der Anwendung das MainView angezeigt wird, es wurde aber das MainWindow angezeigt weil dieses als StartupUri in der Application.xaml angegeben ist.

    In diesem ist aber kein DataContext hinterlegt und auch im Startup Code wird diesem keiner zugewiesen. Also kann der Command nicht funktionieren.

    Ich habe nun in dem MainWindow (im App-Projekt) einen Datenkontext gesetzt und ein DataTemplate angegeben (hattest du noch nirgens oder? Wie soll die WPF dann wissen wie ein MainViewModel gerendert werden soll?) damit das richtige UserControl angezeigt wird. Hierfür dient der ContentPresenter.

    XML-Quellcode

    1. <Window x:Class="MainWindow"
    2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    4. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    5. xmlns:vm="clr-namespace:DialogFensterTest.ViewModel;assembly=DialogFensterTest.ViewModel"
    6. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    7. xmlns:View="clr-namespace:DialogFensterTest.View;assembly=DialogFensterTest.View"
    8. mc:Ignorable="d"
    9. Title="MainWindow" Height="450" Width="800">
    10. <Window.Resources>
    11. <DataTemplate DataType="{x:Type vm:MainViewModel}">
    12. <View:MainView/>
    13. </DataTemplate>
    14. </Window.Resources>
    15. <Window.DataContext>
    16. <vm:MainViewModel/>
    17. </Window.DataContext>
    18. <ContentPresenter Content="{Binding}"></ContentPresenter>
    19. </Window>


    Im UserControl "MainView" kann nun der DataContext entfernt werden, der wird ja vererbt (sonst wird ja eine neue Instanz erzeugt).

    XML-Quellcode

    1. <UserControl x:Class="MainView"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:viewmodel="clr-namespace:DialogFensterTest.ViewModel;assembly=DialogFensterTest.ViewModel"mc:Ignorable="d"d:DesignHeight="450" d:DesignWidth="800"><Grid><Button Height="30" Width="100" Content="Dialog öffnen" Command="{Binding DialogOeffnen}"/></Grid></UserControl>


    Dein DialogFenster habe ich gelöscht und im DialogWindowService auf das SPSWindow ausgebessert. Wozu was neues erstellen.
    Mein SPSWindow hat ein DependencyProperty "AsSPSModalDialog" welches den Owner setzt wenn es sich um einen Dialog handelt.

    Im SPSWindow habe ich das DataTemplate für das "DialogFensterViewModel" definiert.

    XML-Quellcode

    1. <DataTemplate DataType="{x:Type vm:DialogFensterViewModel}">
    2. <Border Height="100" Width="400">
    3. <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding WelcomeText}"/>
    4. </Border>
    5. </DataTemplate>



    Ich hoffe nun wird das ganze klarer.

    Grüße
    Sascha
    Dateien
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    @Nofear23m

    OK alles klar ist soweit angekommen. Das hätte ich vllt sogar selbst hinbekommen. Kenns ja noch von meinem selbstgebauten TabControl ;)

    Viel wichtiger waren mir diese zwei Fragen, bevor ich jetzt weitermach und dann alles nochmal umschreiben muss:

    (1) Ist es mit deinem MultiProject-Template möglich, das kleine Testprogramm dann in eine DLL umzuwandeln, so dass ich es leicht in künftige Projekte integrieren kann oder muss ich da von Anfang an irgendwas anders machen? In WinForms hab ich das schon gemacht, aber geht das auch mit einem MVVM-Projekt?
    Hab dazu folgendes Video gefunden:

    Einfach App.config, App.xaml und MainWindow.xaml entfernen schön und gut, aber woher weiss das Programm dann, mit welchem ViewModel es "anfangen" soll?

    (2) Wie mache ich es, wenn ich einen nicht-modalen Dialog haben will, ich habe vor, eine Fortschrittsanzeige zu machen, und da wäre es Unsinn, den Code anzuhalten, während der Dialog geöffnet ist? Geht das evtl. mit einem einfachen Argument oder muss ich da ein extra Fenster noch hernehmen?
    Kaffee, bevor du nun "weitermachst" solltest du wirklich (und das ist nicht böse gemeint) die Grundlagen lernen und den Code verstehen. Denn die letzten Fragen passen so garnicht zu dem was du "angehen willst".

    Zu 1.)
    Das hat doch mit dem MultiProject-Template zu tun. Du hast ja sowieso wenn du MVVM machst einzelne dlls. Schon mal in deinen Output-Ordner geguckt?
    Das ist die ViewModel und die View dll drinnen.
    Warum willst du irgendwas entfernen? Das App-Projekt ist ne exe, die vergiss gleich mal. (Das ist der Grund warum ich sage das so viel wie möglich in die anderen Projekte soll und nicht in das App-Projekt)
    Deine Controls haste in der View.dll und deine Logik in der ViewModel.dll. Und deine "neue" exe (also das Projekt wo du die dlls verwenden willst) bestimmt dann wieder neu wie z.b. ein Dialogfenster aufgehen soll indem es das DialogWindowService implementiert. Das ist ja der clou an der Sache. Du kannst die selbe Logic (ViewModel) nun z.b. für eine Handyapp oder eine Website verwenden und dort ist es kein Window was sich öffnet sondern etwas anderes weil die "App" dies dann eben anders implementiert.

    Zu 2.)
    Haste doch schon! Das meine ich damit das du es noch nicht verstehst. (Denke ich)
    Du hast ja ein WindowService. (Das von mir)

    Ob in dem Window nun eine Progressbar drinnen ist oder sonstwas ist doch schnuppe. Oder habe ich die Frage falsch verstanden?

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    @Nofear23m

    Na klar hab ich schon mal in die Output-Ordner geguckt. Was ich vorhatte war quasi das komplette Projekt so zu schreiben, dass man es komplett in eine einzige DLL kompilieren kann (also anstatt der *.exe), dass ich die dann in mein(e) Projekt(e) einbinden kann und mit einer einfachen Zeile dann sagen kann z.B. in der Art (Pseudo-Code):

    Imports DialogFenster [...]
    DialogFenster.Show(JaNeinVM, "Möchtest du fortfahren?", [...], [...])

    Ich hab z.B. jetzt in WinForms mal so ne Art VU-Meter-Control gebaut, das mir dann den Pegel meiner Musik anzeigt.

    Also in diesem Fall mit dem Dialogfenster dann ein UserControl, aber ohne eine View, bzw. die View erst dann zum Vorschein kommt, wenn ich sage DialogFenster.Show

    Mir ist bloss nicht klar, wie ich praktisch an die Sache rangehen soll. Was ich da bei dir so raushöre ist (und da kann ich mich täuschen), dass das so nicht möglich ist, und dass ich bei jedem Projekt, in dem ich das verwenden will, von Hand einbauen muss (also ich meine die ganzen Steps, die in dem Thread von VaporiZed enthalten sind, z. B. die Schnittstelle usw.)

    Ich glaube du verstehst was ich meine...


    _________________________________________________

    Nofear23m schrieb:

    Du hast ja ein WindowService. (Das von mir)


    Ah okay, also statt dass ich...

    VB.NET-Quellcode

    1. Dim dialogService = Services.ServiceContainer.GetService(Of Services.IDialogWindowService)
    2. dialogService.ShowModalDialog("newAp", vm, Me, True, True)


    aufrufe, dann einfach...
    Dim dialogService = Services.ServiceContainer.GetService(Of Services.IWindowService) dialogService.OpenWindow([...])

    So meinst du?

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „kafffee“ ()

    kafffee schrieb:

    Was ich da bei dir so raushöre ist (und da kann ich mich täuschen), dass das so nicht möglich ist, und dass ich bei jedem Projekt, in dem ich das verwenden will, von Hand einbauen muss...

    Da kommt ganz darauf an wie DU es abstrahierst.
    Wenn du die ServiceImplementierung auch in eine extra Assembly packst ist das kein Problem.
    Aber das was du vorhast (ja, es ginge) ist ein totale unart wenn man eine exe als dll verfügbar machen will.
    Deshalb gibt es ja Assemblys welche man referenzieren kann. Abstrahierst du deinen Code hast du ihn auch woanders zur Verfügung, es ist immer das selbe Prinzip.

    kafffee schrieb:

    So meinst du?

    Richtig, nur die Variablennamen bitte auch ändern. Wieder das Thema "Ordnung".

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Hallo

    Bist du nun eigentlich zum Ziel gekommen? Hast du dein Dialogfenster?
    Oder habe ich dich verschreckt ;)

    Berichte evtl. kurz und vielleicht postest du nochmals den aktuellen Stand in Form eines Uploads, dann können wir das nochmal final durchgehen ob alles passt.

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Haha :D hast mich schon vermisst? Keine Sorge meine Mühlen laufen nur etwas langsam...

    Nofear23m schrieb:

    Da kommt ganz darauf an wie DU es abstrahierst.
    Wenn du die ServiceImplementierung auch in eine extra Assembly packst ist das kein Problem.
    Aber das was du vorhast (ja, es ginge) ist ein totale unart wenn man eine exe als dll verfügbar machen will.
    Deshalb gibt es ja Assemblys welche man referenzieren kann. Abstrahierst du deinen Code hast du ihn auch woanders zur Verfügung, es ist immer das selbe Prinzip.


    Das hier hat mich tatsächlich etwas erschreckt... Sehr viele Fremdwörter, aber da möchte ich später noch mal drauf zurückkommen, das Ganze mit den DLLs und der Portierbarkeit...

    Hab mich grad nochmal drei Stunden hingesetzt und alles soweit nochmals aufgesetzt aber es will noch nicht so richtig funktionieren. Ich gehe davon aus dass der Fehler in den XAMLs ist. Bin mir nicht sicher ob ich die DataTemplates richtig verwende... Auch Probleme hab ich mit der Private Sub JaNeinDialogOeffnen_Execute. Das kann ja gar nicht funktionieren, so wie ich das jetzt hab.

    Das Ganze sieht so aus:
    -Das MainWindow ist bloss zu Testzwecken da sozusagen zum Aufrufen der Dialoge beim Klick auf einen der Buttons (bisher ist nur der für Ja/Nein-Dialoge mit einem Binding versehen)
    -Ich habe zwei Dialoginhalte als Usercontrol, einmal JaNeinDialog und einmal OKDialog in der View
    -Diese sollen dann im Fenster DialogFenster entweder oder angezeigt werden, je nachdem was/wie man aufruft
    -Dann habe ich ein StatusFenster, in dem das StatusFensterUC angezeigt werden soll, und zwar ohne dass der Code anhält, das ist aber noch nicht fertig, also ignoriere das erstmal

    --> Wenn ich jetzt auf den Button zum Öffnen des Ja/Nein Dialogs klicke, wird das Fenster zwar geöffnet, aber es wird wieder mal nix angezeigt. Erstes Problem wird sein, dass ich wie gesagt glaube ich die DataTemplates/ContentPresenter falsch verwende. Aber was mir noch spanisch vorkommt, ist dass das geöffnete DialogFenster nicht mal in der Grösse öffnet bzw. überhaupt mit den Eigenschaften (WindowStartupLocation etc...) wird, die ich in seinen XAML-Eigenschaften festgelegt hab...

    Ich bin Datei für Datei durchgegangen und hab alles auch mit deinem hochgeladenen Beispiel verglichen, aber ich weiss jetzt nicht mehr weiter...

    Bitte lass dich nicht verwirren in der Zeile 19 des MainViewModels das ist mir klar dass das an dieser Stelle nicht funktionieren kann. Ich hatte eigentlich so gedacht, dass man dann als Argument entweder das OKDialogViewModel oder das JaNeinDialogViewModel übergibt und dann im Konstruktor festlegt, welches von beiden der Contentpresenter anzeigen soll... Aber jetzt kommt erstmal das Problem, dass das Fenster wie gesagt nicht mit den richtigen Eigenschaften angezeigt wird...

    Freue mich auf deine Antwort,

    kafffee

    PS: Ich les mir jetzt auch noch mal dein Kapitel über DataTemplates durch, kann nicht schaden...
    Dateien

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „kafffee“ ()

    Hallo

    Also ich habs jetzt mal kurz "überflogen".
    Ich denke du hast dich da "verhaspelt" wodurch es unnötig kompliziert wurde.

    Verstehe allerdings warum das passiert ist. Dein Gedankengang war ja eben das du eine eigene Messagebox erstellen willst und dadurch quasi X DataTemplates benötigst (für die Kombinationen JaNein, OK, OK+Cancel usw) und da hast du dich eben verrannt. Ist aber auch kein einfaches Thema ansich, und dann auch noch mit MVVM wo du ja noch nicht fit bist.

    Du hast aber nun endgültig meine Neugier geweckt und ich würde gerne mal dein Projekt so umbauen das es wirklich sauber implementiert ist.

    Und ja, schau dir DataTemplates nochmals an, das ist eh der Punkt wo du dich etwas verrannt hast.

    So, ich versuch das mal richtig sauber hinzubekommen (damit es auch Testbar wäre) und stell das dann Online.

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Eine Messagebox in MVVM

    So Leute, ich habe mir da mal ein wenig mehr Arbeit gemacht und wirklich eine eigene Messagebox erstellt welche MVVM Konform ist weil es mich einfach interessiert hat.

    Was sollte diese können:

    Die Standardfunktionalitäten:
    • Titel
    • Text
    • Buttons
    • Result als Rückgabe
    Was sie mehr als eine normale Box können soll:
    • Ich wollte das man von Außen eigene Buttons implementieren kann (inkl. eigenem Style per DataTemplate)
    • Ich wollte das man von Außen eigenen Content Rendern kann (Also statt dem Text)
    • Ich wollte von Außen die Reihenfolge der Custom-Buttons beeinflussen können
    • Ich wollte eigene Icons implementieren

    Ich habe erstmal folgende Interface implementiert (ICustomMessageBoxService):

    VB.NET-Quellcode

    1. Namespace Services
    2. Public Interface ICustomMessageBoxService
    3. Function Show(
    4. message As String,
    5. title As String,
    6. Optional buttons As EnuMessageBoxButton = 0,
    7. Optional image As EnuMessageBoxImage = 0,
    8. Optional owner As Object = Nothing) As EnuMessageBoxResult
    9. Function Show(
    10. customContent As MsgBoxContentViewModel,
    11. title As String,
    12. Optional buttons As EnuMessageBoxButton = 0,
    13. Optional image As EnuMessageBoxImage = 0,
    14. Optional owner As Object = Nothing) As EnuMessageBoxResult
    15. Function Show(
    16. message As String,
    17. title As String,
    18. buttonContent As MsgBoxButtonsViewModel,
    19. Optional image As EnuMessageBoxImage = 0,
    20. Optional owner As Object = Nothing) As EnuMessageBoxResult
    21. Function Show(
    22. customContent As MsgBoxContentViewModel,
    23. title As String,
    24. buttonContent As MsgBoxButtonsViewModel,
    25. Optional image As EnuMessageBoxImage = 0,
    26. Optional owner As Object = Nothing) As EnuMessageBoxResult
    27. Function Show(
    28. customContent As MsgBoxContentViewModel,
    29. title As String,
    30. buttons As List(Of MsgButton),
    31. Optional image As EnuMessageBoxImage = 0,
    32. Optional owner As Object = Nothing) As EnuMessageBoxResult
    33. Function Show(
    34. message As String,
    35. title As String,
    36. buttons As List(Of MsgButton),
    37. Optional image As EnuMessageBoxImage = 0,
    38. Optional owner As Object = Nothing) As EnuMessageBoxResult
    39. Sub CloseWindow()
    40. End Interface
    41. Public Enum EnuMessageBoxResult
    42. None = 0
    43. Ok = 1
    44. Cancel = 2
    45. Yes = 6
    46. No = 7
    47. Custom = 8
    48. End Enum
    49. Public Enum EnuMessageBoxButton
    50. Ok = 0
    51. OkCancel = 1
    52. YesNoCancel = 3
    53. YesNo = 4
    54. End Enum
    55. Public Enum EnuMessageBoxImage
    56. None = 0
    57. [Error] = 16
    58. Question = 32
    59. Warning = 48
    60. Information = 64
    61. End Enum


    Dieses impliziert das folgende ViewModels benötigt werden um die Zusatzfunktionalitäten zu erhalten:

    • Ein MessageBoxContentViewModel - Dieses kann einen Content halten. Entweder ist dies ein Text oder ein ViewModel (welches dann ja wiederum per DataTemplate gerendert wird wie man will)

    VB.NET-Quellcode

    1. Public Class MsgBoxContentViewModel
    2. Inherits ViewModelBase
    3. Public Sub New()
    4. End Sub
    5. Public Sub New(content As ViewModelBase)
    6. Me.Content = content
    7. End Sub
    8. Private _Content As Object
    9. Public Property Content As Object
    10. Get
    11. Return _Content
    12. End Get
    13. Set
    14. _Content = Value
    15. RaisePropertyChanged()
    16. End Set
    17. End Property
    18. End Class

    • Ein MsgBoxButtonsViewModel - Je nach verwendetem Konstruktor kann hier entschieden werden ob man die Standard-Buttons oder eine Auflistung eigener Buttons angezeigt haben möchte.

    VB.NET-Quellcode

    1. Public Class MsgBoxButtonsViewModel
    2. Inherits ViewModelBase
    3. Public Event RequestClose()
    4. Public Sub New()
    5. Me.New(Services.EnuMessageBoxButton.Ok)
    6. End Sub
    7. Public Sub New(buttonsType As Services.EnuMessageBoxButton)
    8. Buttons = New List(Of MsgButton)
    9. Select Case buttonsType
    10. Case Services.EnuMessageBoxButton.Ok
    11. Buttons.Add(New MsgButton() With {.Content = "OK", .Command = New RelayCommand(Sub() GetResult(Services.EnuMessageBoxResult.Ok))})
    12. Case Services.EnuMessageBoxButton.OkCancel
    13. Buttons.Add(New MsgButton() With {.Content = "OK", .Command = New RelayCommand(Sub() GetResult(Services.EnuMessageBoxResult.Ok))})
    14. Buttons.Add(New MsgButton() With {.Content = "Abbrechen", .Command = New RelayCommand(Sub() GetResult(Services.EnuMessageBoxResult.Cancel))})
    15. Case Services.EnuMessageBoxButton.YesNo
    16. Buttons.Add(New MsgButton() With {.Content = "Ja", .Command = New RelayCommand(Sub() GetResult(Services.EnuMessageBoxResult.Yes))})
    17. Buttons.Add(New MsgButton() With {.Content = "Nein", .Command = New RelayCommand(Sub() GetResult(Services.EnuMessageBoxResult.No))})
    18. Case Services.EnuMessageBoxButton.YesNoCancel
    19. Buttons.Add(New MsgButton() With {.Content = "Ja", .Command = New RelayCommand(Sub() GetResult(Services.EnuMessageBoxResult.Yes))})
    20. Buttons.Add(New MsgButton() With {.Content = "Nein", .Command = New RelayCommand(Sub() GetResult(Services.EnuMessageBoxResult.No))})
    21. Buttons.Add(New MsgButton() With {.Content = "Abbruch", .Command = New RelayCommand(Sub() GetResult(Services.EnuMessageBoxResult.Cancel))})
    22. Case Else
    23. Throw New Exception("Ungültiger Buttonstype übergeben")
    24. End Select
    25. End Sub
    26. Private Sub GetResult(Result As Services.EnuMessageBoxResult)
    27. Me.Result = Result
    28. RaiseEvent RequestClose()
    29. End Sub
    30. Public Sub New(customButtons As List(Of MsgButton))
    31. Buttons = New List(Of MsgButton)
    32. For Each cb In customButtons
    33. Buttons.Add(cb)
    34. Next
    35. End Sub
    36. Private _buttons As List(Of MsgButton)
    37. Public Property Buttons() As List(Of MsgButton)
    38. Get
    39. Return _buttons
    40. End Get
    41. Set(ByVal value As List(Of MsgButton))
    42. _buttons = value
    43. RaisePropertyChanged()
    44. End Set
    45. End Property
    46. Friend Result As Services.EnuMessageBoxResult = Services.EnuMessageBoxResult.None
    47. End Class
    48. Public Class MsgButton
    49. Public Property Content As Object
    50. Public Property Command As ICommand
    51. End Class


    Dann war es an der Zeit das Interface zu implementieren. Das unterscheidet sich nicht sonderlich zu einem anderen DialogWindowService welches wir hier ja bereits durch haben. Lediglich das es verschiedene überladungen gibt welche im Interface ja auch so festgelegt wurden. Klar, sonst würden wir diese ja im ViewModel dann ja nicht kennen, wir kennen ja nur das Interface.

    VB.NET-Quellcode

    1. Public Class CustomMessageBoxService
    2. Implements ICustomMessageBoxService
    3. Dim _currentWindow As View.SpsWindow = Nothing
    4. Public Sub CloseWindow() Implements ICustomMessageBoxService.CloseWindow
    5. _currentWindow.Close()
    6. End Sub
    7. Public Function Show(
    8. message As String,
    9. title As String,
    10. Optional buttons As EnuMessageBoxButton = EnuMessageBoxButton.Ok,
    11. Optional image As EnuMessageBoxImage = EnuMessageBoxImage.None,
    12. Optional owner As Object = Nothing) As EnuMessageBoxResult Implements ICustomMessageBoxService.Show
    13. Return Show(New MsgBoxContentViewModel() With {.Content = message}, title, New MsgBoxButtonsViewModel(buttons), image, owner)
    14. End Function
    15. Public Function Show(
    16. customContent As MsgBoxContentViewModel,
    17. title As String,
    18. Optional buttons As EnuMessageBoxButton = EnuMessageBoxButton.Ok,
    19. Optional image As EnuMessageBoxImage = EnuMessageBoxImage.None,
    20. Optional owner As Object = Nothing) As EnuMessageBoxResult Implements ICustomMessageBoxService.Show
    21. Return Show(customContent, title, New MsgBoxButtonsViewModel(buttons), image, owner)
    22. End Function
    23. Public Function Show(
    24. message As String,
    25. title As String,
    26. buttonContent As MsgBoxButtonsViewModel,
    27. Optional image As EnuMessageBoxImage = EnuMessageBoxImage.None,
    28. Optional owner As Object = Nothing) As EnuMessageBoxResult Implements ICustomMessageBoxService.Show
    29. Return Show(New MsgBoxContentViewModel() With {.Content = message}, title, buttonContent, image, owner)
    30. End Function
    31. Public Function Show(
    32. customContent As MsgBoxContentViewModel,
    33. title As String,
    34. buttonContent As MsgBoxButtonsViewModel,
    35. Optional image As EnuMessageBoxImage = EnuMessageBoxImage.None,
    36. Optional owner As Object = Nothing) As EnuMessageBoxResult Implements ICustomMessageBoxService.Show
    37. Dim msgVm = New MsgBoxViewModel(customContent, image, buttonContent)
    38. Dim dlgwin As New View.SpsWindow("custommessagebox", msgVm, SizeToContent.WidthAndHeight, WindowStartupLocation.CenterOwner)
    39. _currentWindow = dlgwin
    40. dlgwin.Topmost = True
    41. dlgwin.Title = title
    42. dlgwin.CanCloseWithEsc = True
    43. dlgwin.MaxWidth = 600
    44. dlgwin.MaxHeight = 500
    45. dlgwin.WindowStyle = WindowStyle.ToolWindow
    46. dlgwin.ShowInTaskbar = True
    47. dlgwin.Owner = dlgwin.FindOwnerWindow(owner)
    48. dlgwin.WindowStartupLocation = WindowStartupLocation.CenterOwner
    49. Application.Current.Dispatcher.Invoke(Function() dlgwin.ShowDialog)
    50. Return msgVm.Result
    51. End Function
    52. Public Function Show(
    53. message As String,
    54. title As String,
    55. buttons As List(Of MsgButton),
    56. Optional image As EnuMessageBoxImage = EnuMessageBoxImage.None,
    57. Optional owner As Object = Nothing) As EnuMessageBoxResult Implements ICustomMessageBoxService.Show
    58. Return Show(New MsgBoxContentViewModel() With {.Content = message}, title, buttons, image, owner)
    59. End Function
    60. Public Function Show(
    61. customContent As MsgBoxContentViewModel,
    62. title As String,
    63. buttons As List(Of MsgButton),
    64. Optional image As EnuMessageBoxImage = EnuMessageBoxImage.None,
    65. Optional owner As Object = Nothing) As EnuMessageBoxResult Implements ICustomMessageBoxService.Show
    66. Dim msgbVm = New MsgBoxButtonsViewModel(buttons)
    67. Dim msgVm = New MsgBoxViewModel(customContent, image, msgbVm)
    68. Dim dlgwin As New View.SpsWindow("custommessagebox", msgVm, SizeToContent.WidthAndHeight, WindowStartupLocation.CenterOwner)
    69. _currentWindow = dlgwin
    70. dlgwin.Topmost = True
    71. dlgwin.Title = title
    72. dlgwin.MaxWidth = 600
    73. dlgwin.MaxHeight = 500
    74. dlgwin.CanCloseWithEsc = True
    75. dlgwin.WindowStyle = WindowStyle.ToolWindow
    76. dlgwin.ShowInTaskbar = True
    77. dlgwin.Owner = dlgwin.FindOwnerWindow(owner)
    78. dlgwin.WindowStartupLocation = WindowStartupLocation.CenterOwner
    79. Application.Current.Dispatcher.Invoke(Function() dlgwin.ShowDialog)
    80. Return msgVm.Result
    81. End Function
    82. End Class


    Für alle drei Klassen habe ich dann mal DataTemplate mit den dazu passenden Views erstellt:

    XML-Quellcode

    1. <DataTemplate DataType="{x:Type vmInfr:MsgBoxViewModel}">
    2. <View:uclCustomMsgBoxView Content="{Binding}"/>
    3. </DataTemplate>
    4. <DataTemplate DataType="{x:Type viewmodel:CustomMessagboxButtonTestViewModel}">
    5. <View:uclCustomMessageboxButtonTest/>
    6. </DataTemplate>
    7. <DataTemplate DataType="{x:Type viewmodel:CustomMsgBoxContentViewModel}">
    8. <View:uclCustomMsgBoxContentView/>
    9. </DataTemplate>


    Ich gehe hier jetzt aber nur auf die uclCustomMsgBoxView.xaml ein da die andern unspektakulär sind und vielleicht drei oder vier Zeilen enthalten:

    Spoiler anzeigen

    XML-Quellcode

    1. <UserControl x:Class="uclCustomMsgBoxView"
    2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    4. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    5. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    6. xmlns:local="clr-namespace:DialogFensterTest.View"
    7. xmlns:s="clr-namespace:System;assembly=mscorlib"
    8. xmlns:vmInfr="clr-namespace:DialogFensterTest.ViewModel.Instrastructure;assembly=DialogFensterTest.ViewModel"
    9. xmlns:vmServ="clr-namespace:DialogFensterTest.ViewModel.Services;assembly=DialogFensterTest.ViewModel"
    10. mc:Ignorable="d" d:DataContext="{d:DesignInstance IsDesignTimeCreatable=True,Type={x:Type vmInfr:MsgBoxViewModel}}"
    11. d:DesignHeight="182.143" d:DesignWidth="603.246" Background="AliceBlue">
    12. <UserControl.Resources>
    13. <Style TargetType="TextBlock">
    14. <Setter Property="TextWrapping" Value="Wrap"/>
    15. <Setter Property="FontSize" Value="20"/>
    16. </Style>
    17. <DataTemplate DataType="{x:Type vmInfr:MsgBoxButtonsViewModel}">
    18. <ContentControl>
    19. <ItemsControl ItemsSource="{Binding Buttons}" HorizontalAlignment="Right" Background="Transparent" BorderThickness="0" Margin="10,5">
    20. <ItemsControl.ItemsPanel>
    21. <ItemsPanelTemplate>
    22. <WrapPanel Orientation="Horizontal" IsItemsHost="True" HorizontalAlignment="Right" Background="Transparent">
    23. <WrapPanel.Resources>
    24. <DataTemplate DataType="{x:Type vmInfr:MsgButton}">
    25. <Button Content="{Binding Content}" Command="{Binding Command}" Margin="5" Padding="5" FontWeight="Bold"/>
    26. </DataTemplate>
    27. </WrapPanel.Resources>
    28. </WrapPanel>
    29. </ItemsPanelTemplate>
    30. </ItemsControl.ItemsPanel>
    31. </ItemsControl>
    32. </ContentControl>
    33. </DataTemplate>
    34. <DataTemplate DataType="{x:Type vmInfr:MsgBoxContentViewModel}">
    35. <ContentControl FontSize="20" Content="{Binding Content}" Margin="20">
    36. <ContentControl.Resources>
    37. <DataTemplate DataType="{x:Type vmInfr:ViewModelBase}">
    38. <ContentControl FontSize="20" Content="{Binding Content}" Margin="20"/>
    39. </DataTemplate>
    40. <DataTemplate DataType="{x:Type s:String}">
    41. <TextBlock FontSize="20" Text="{Binding}" TextWrapping="Wrap"/>
    42. </DataTemplate>
    43. </ContentControl.Resources>
    44. </ContentControl>
    45. </DataTemplate>
    46. </UserControl.Resources>
    47. <Grid>
    48. <Grid.RowDefinitions>
    49. <RowDefinition Height="*"/>
    50. <RowDefinition Height="Auto"/>
    51. </Grid.RowDefinitions>
    52. <Grid.ColumnDefinitions>
    53. <ColumnDefinition Width="Auto"/>
    54. <ColumnDefinition Width="*"/>
    55. </Grid.ColumnDefinitions>
    56. <ContentPresenter Content="{Binding Content}" Grid.Column="1">
    57. <ContentPresenter.Resources>
    58. <Style TargetType="TextBlock">
    59. <Style.Setters>
    60. <Setter Property="FontSize" Value="20"/>
    61. <Setter Property="TextWrapping" Value="Wrap"/>
    62. <Setter Property="Margin" Value="10"/>
    63. </Style.Setters>
    64. </Style>
    65. </ContentPresenter.Resources>
    66. </ContentPresenter>
    67. <ContentPresenter Content="{Binding ButtonContent}" Grid.ColumnSpan="2" Grid.Row="1"/>
    68. <ContentControl Width="120" Height="120">
    69. <ContentControl.Style>
    70. <Style TargetType="ContentControl">
    71. <Setter Property="Content">
    72. <Setter.Value>
    73. <Viewbox Width="100" Margin="20,0,0,0" Stretch="Uniform">
    74. <Path Fill="Black" Data="M13 11H11V5H13M13 15H11V13H13M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2Z" />
    75. </Viewbox>
    76. </Setter.Value>
    77. </Setter>
    78. <Style.Triggers>
    79. <DataTrigger Binding="{Binding Image}" Value="{x:Static vmServ:EnuMessageBoxImage.None}">
    80. <Setter Property="Content">
    81. <Setter.Value>
    82. <Viewbox Width="100" Margin="20,0,0,0" Stretch="Uniform">
    83. <Path Fill="Black" Data="M13 11H11V5H13M13 15H11V13H13M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2Z" />
    84. </Viewbox>
    85. </Setter.Value>
    86. </Setter>
    87. <Setter Property="Margin" Value="0"></Setter>
    88. <Setter Property="Width" Value="60"></Setter>
    89. <Setter Property="Height" Value="60"></Setter>
    90. </DataTrigger>
    91. <DataTrigger Binding="{Binding Image}" Value="{x:Static vmServ:EnuMessageBoxImage.Information}">
    92. <Setter Property="Content">
    93. <Setter.Value>
    94. <Viewbox Width="100" Margin="20,0,0,0" Stretch="Uniform">
    95. <Path Fill="Blue" Data="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" />
    96. </Viewbox>
    97. </Setter.Value>
    98. </Setter>
    99. </DataTrigger>
    100. <DataTrigger Binding="{Binding Image}" Value="{x:Static vmServ:EnuMessageBoxImage.Error}">
    101. <Setter Property="Content">
    102. <Setter.Value>
    103. <Viewbox Width="100" Margin="20,0,0,0" Stretch="Uniform">
    104. <Path Fill="DarkRed" Data="M13 14H11V9H13M13 18H11V16H13M1 21H23L12 2L1 21Z" />
    105. </Viewbox>
    106. </Setter.Value>
    107. </Setter>
    108. </DataTrigger>
    109. <DataTrigger Binding="{Binding Image}" Value="{x:Static vmServ:EnuMessageBoxImage.Question}">
    110. <Setter Property="Content">
    111. <Setter.Value>
    112. <Viewbox Width="100" Margin="20,0,0,0" Stretch="Uniform">
    113. <Path Fill="DarkSeaGreen" Data="M4,2A2,2 0 0,0 2,4V16A2,2 0 0,0 4,18H8V21A1,1 0 0,0 9,22H9.5V22C9.75,22 10,21.9 10.2,21.71L13.9,18H20A2,2 0 0,0 22,16V4C22,2.89 21.1,2 20,2H4M4,4H20V16H13.08L10,19.08V16H4V4M12.19,5.5C11.3,5.5 10.59,5.68 10.05,6.04C9.5,6.4 9.22,7 9.27,7.69C0.21,7.69 6.57,7.69 11.24,7.69C11.24,7.41 11.34,7.2 11.5,7.06C11.7,6.92 11.92,6.85 12.19,6.85C12.5,6.85 12.77,6.93 12.95,7.11C13.13,7.28 13.22,7.5 13.22,7.8C13.22,8.08 13.14,8.33 13,8.54C12.83,8.76 12.62,8.94 12.36,9.08C11.84,9.4 11.5,9.68 11.29,9.92C11.1,10.16 11,10.5 11,11H13C13,10.72 13.05,10.5 13.14,10.32C13.23,10.15 13.4,10 13.66,9.85C14.12,9.64 14.5,9.36 14.79,9C15.08,8.63 15.23,8.24 15.23,7.8C15.23,7.1 14.96,6.54 14.42,6.12C13.88,5.71 13.13,5.5 12.19,5.5M11,12V14H13V12H11Z" />
    114. </Viewbox>
    115. </Setter.Value>
    116. </Setter>
    117. </DataTrigger>
    118. <DataTrigger Binding="{Binding Image}" Value="{x:Static vmServ:EnuMessageBoxImage.Warning}">
    119. <Setter Property="Content">
    120. <Setter.Value>
    121. <Viewbox Width="100" Margin="20,0,0,0" Stretch="Uniform">
    122. <Path Fill="Orange" Data="M8.27,3L3,8.27V15.73L8.27,21H15.73C17.5,19.24 21,15.73 21,15.73V8.27L15.73,3M9.1,5H14.9L19,9.1V14.9L14.9,19H9.1L5,14.9V9.1M11,15H13V17H11V15M11,7H13V13H11V7" />
    123. </Viewbox>
    124. </Setter.Value>
    125. </Setter>
    126. </DataTrigger>
    127. </Style.Triggers>
    128. </Style>
    129. </ContentControl.Style>
    130. </ContentControl>
    131. </Grid>
    132. </UserControl>


    Im Grunde keine Magie, ich setzte die DataTemplates für MsgBoxButtons sowie für MsgBoxButtonsViewModel und für MsgBoxContentViewModel.

    Im DataTemplate von MsgBoxContentViewModel habe ich dann einen Trigger welcher unterschiedliche Controls Rendert je nachdem ob es sich um einen String (normaler Text) oder um einen Custom-Content handelt.
    Ansonsten gibt es einfach nur noch Trigger welche das Icon setzen, je nachdem welches Icon angegeben wurde.

    Sehen wir uns an wie das ganz nun verwendet wird:

    Um eine normale Messagebox mit Ja/Nein zu öffnen einfach wie folgt:

    VB.NET-Quellcode

    1. Dim dialogService = Services.ServiceContainer.GetService(Of Services.ICustomMessageBoxService)
    2. Dim result = dialogService.Show("Die Nachricht", "Der Titel", Services.EnuMessageBoxButton.YesNo, Services.EnuMessageBoxImage.Information)


    Will man sowohl die Buttons, als auch den Content als Custom haben wie folgt:

    VB.NET-Quellcode

    1. Dim dialogService = Services.ServiceContainer.GetService(Of Services.ICustomMessageBoxService)
    2. Dim result As Services.EnuMessageBoxResult = Services.EnuMessageBoxResult.None
    3. Dim customButtonCommand1 As ICommand = New RelayCommand(AddressOf WriteToConsoleAndCloseWindow)
    4. Dim btnOne = New MsgButton() With {.Command = customButtonCommand1, .Content = New CustomMessagboxButtonTestViewModel("Sehr cool")}
    5. Dim buttonsVm = New MsgBoxButtonsViewModel(New List(Of MsgButton) From {btnOne})
    6. result = dialogService.Show(New MsgBoxContentViewModel(New CustomMsgBoxContentViewModel()), "Der Titel", buttonsVm, Services.EnuMessageBoxImage.Error)


    Anbei noch ein paar Screenshots wie das aussehen kann. Feedback, Log, Kritik. Alles erwünscht!! ;)

    Grüße
    Sascha
    Bilder
    • 5.png

      213,95 kB, 729×488, 62 mal angesehen
    • 4.png

      59,76 kB, 719×390, 54 mal angesehen
    • 3.png

      46,47 kB, 669×353, 56 mal angesehen
    • 2.png

      67,33 kB, 780×439, 57 mal angesehen
    • 1.png

      49,04 kB, 782×435, 55 mal angesehen
    Dateien
    • CustomMsgBox.zip

      (137,54 kB, 65 mal heruntergeladen, zuletzt: )
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Nofear23m“ ()

    Nofear23m schrieb:

    Feedback, Log, Kritik. Alles erwünscht!!


    Allererst mal ein Lob. Da hast du dich richtig ins Zeug gelegt. Einfach mal so kurz aus dem Ärmel geschüttelt...

    Zu kritisieren gibt es erstmal nichts, bis auf die Tatsache dass es für mich zu viel auf einmal ist, wenn ich es verstehen will. Ist super, wenn mans 1 zu 1 in sein Projekt einfügen will, aber wie du mich wahrscheinlich kennst, tu ich das meistens ungern, sondern versuch immer das dann wenigstens "in eigenen Worten" nochmal zu rewriten und es auf meine Art zu interpretieren... Ich guck mir dein Projekt jetzt eine halbe oder dreiviertel Stunde an, aber mit leerem Kopf, d.h. mir fallen keine konkreten Fragen ein... Es sind nicht so sehr die einzelnen Prozeduren, sondern eher der Gesamtzusammenhang...

    Deshalb mein Wunsch:
    Könntest du mal nochmal in mein hochgeladenes Projekt aus Post#30 reinschauen, und (mit der bereits vorhandenen von mir erstellten Infrastruktur) mal soweit fertig machen, dass wenigstens mein DialogFenster.xaml richtig und mit dem Content JaNeinDialog.xaml angezeigt wird, und vielleicht ein paar Kommentare einfügen? Den Rest wie z.B. die Rückgabe eines Wertes würde ich gern erstmal noch selbst probieren. Also nur ganz minimalistisch...

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „kafffee“ ()

    Aber das ist genau das Projekt aus dem Post. Ich habe nur alles unnötige gelöscht.
    Wozu brauchst du ein "DialogFenster" wenn du ja das SPSWindow im Projekt hast welches das übernimmt. Dieses ist so konzipiert das es für normale Fenster UND für Dialoge verwendet werden kann. Also sparen wir uns die Redundanz.

    Im Grunde ist es dein Projekt, aber die ganzen von dir erstellten Dinge habe ich eben selbst interpretiert - unter anderem weil ich mit deutschen Benamsungen nicht klar komme, da stellen sich bei mir immer die Haare auf. Haha ;)

    Die Erklärung wie das alles zusammenhängt hast du im Post von mir, da haben ich versucht zu erklären wie die einzelnen Klassen zusammenarbeiten.

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Ich bin ein bischen raus aus dem Thema aber möchte versuchen das zu beantworten:
    Der DataContext sagt dem XAML, also deiner MainView an welches Model es binden soll, nämlich an MainViewModel.
    IsDesignTimeCreatable sagt deinem XAML, dass es schon zur Designzeit Daten anzeigen soll, so kannst du mit Dummydaten schon was sehen, was da geht.

    Haut mich ruhig wenns falsch ist, ist grade eine Übung für mich, ob ich WPF noch einigermaßen verstehe. Leider komme ich nicht dazu mit WPF was zu machen.
    Naja, ich schrieb ja schon, ich habe WPF schon lange nichts mehr selbst gemacht und habe auch erst mit @Nofear23m's Tutorial angefangen.
    Nun habe ich mir das Projekt mal eben angesehen und stelle fest:
    Die MainView ist ja ein Control, also schon mal kein Window. Und man kann gut experimentieren damit. Also z.B. die Vorgaben im MainViewModel.vb abändern und neu compilieren und gucken.
    MainWindow,xaml soll die MainView.xaml als Resource einbinden, dazu braucht es die Window.Resource. Kennt es aber über den Window.DataContext das viewmodel nicht, kann es gar nichts einbinden.
    So zumindest meine Tests, einfach auskommentieren, compilieren und gucken.

    Ich versuche es ja auch nur zu verstehen.

    kafffee schrieb:

    Macht das also das Gleiche wie:

    Ne. Da muss man aufpassen.

    @Dksksm hat hier den DataContext mit dem Design-DataContext vermischt.

    Die von dir angesprochene Zeile erstellt einen DesignTime-Datenkontext.
    Das bedeutet das hier zur Designzeit (!!) (was durch das d: spezifiziert wird) der Parameterlose Konstruktor der Klasse aufgerufen wird, damit du für die Designzeit daten bereitstellen kannst.
    Aber nicht nur das. Dadurch das der Designer nun zur Designzeit die Klasse und damit dessen Struktur kennt hast du fortan Intellisense fürs Binding. Probiers mal aus ;)

    Den DataContext als solches zu setzen hätte nämlich zur folge das zur Laufzeit (!!) eine Instanz der Klasse über den parameterlosen Konstruktor erstellt werden würde und das Control auf diesen gebunden wäre und ein Binding über dein ViewModel sohin nicht mehr greift. Wäre ja doof in diesem Fall.

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##