Laden...

Forenbeiträge von Palladin007 Ingesamt 2.079 Beiträge

16.03.2024 - 09:04 Uhr

Ok, es hat sich als bescheuerter Fehler herausgestellt ...

Es hat einfach nur folgendes gefehlt:

permissions:
  contents: write

Danach läuft die Authentifizierung automatisch ...

13.03.2024 - 18:10 Uhr

Die InProcess-Implementierung ist trivial zu coden.

Nur wenn es ein Unary-Call ist.

Bei Streaming-Calls unterscheidet sich der Client-Aufruf mit der Server-Implementierung sehr.

// Server

public override async Task DuplexStreamingCall(IAsyncStreamReader<Request> requestStream, IServerStreamWriter<Response> responseStream, ServerCallContext context)
{
    while (await requestStream.MoveNext())
    {
        _logger.LogInformation("DuplexStreamingCall: {RequestData}", requestStream.Current.RequestData);

        var response = new Response
        {
            ResponseData = $"Duplex response to {requestStream.Current.RequestData}"
        };

        await responseStream.WriteAsync(response);

        await Task.Delay(100);
    }
}

// Client

public static async Task DuplexStreamingCall(TestService.TestServiceClient client)
{
    using (var call = client.DuplexStreamingCall())
    {
        var responseTask = Task.Run(async () =>
        {
            await foreach (var response in call.ResponseStream.ReadAllAsync())
            {
                Console.WriteLine($"Duplex Streaming Response: {response.ResponseData}");
            }
        });

        for (int i = 0; i < 5; i++)
        {
            await call.RequestStream.WriteAsync(new Request { RequestData = $"Message {i}" });
            await Task.Delay(100);
        }

        await call.RequestStream.CompleteAsync();
        await responseTask;
    }
}

Insofern wäre die mMn. einzige wirklich praktikable Option um sowas zu erreichen, das Mediator-Pattern. Die Services nutzen dann den Mediator und geben die Daten durch und wenn die Arbeit im selben Prozess stattfinden soll, wird der Mediator (hinter einer Abstraktion natürlich) aus dem Client heraus genutzt und kann sich dabei auch um den DI-Scope kümmern.

DI-Scope muss verwaltet werden

Damit meine ich, dass bei einer Client-Server-Architektur jeder Request/Call auf dem Server in einem DI-Scope resultiert.
Wenn ich nun auf eine reine Client-Architektur umstellen will und dafür eine einfache Implementierung schaffe, fällt das weg, das muss ich dann auch erst umsetzen. Mache ich das nicht, dann habe ich unterschiedliches Verhalten zwischen den zwei Varianten mit potentiell weitreichenden Auswirkungen.

Nach meiner Erfahrung wird eben eine proto eher selten geändert

Mit proto habe ich keine Erfahrungen, aber wenn ich meine Erfahrungen mit REST darauf übertrage, sind meine Erfahrungen sehr anders.
Tatsächlich muss bei meiner derzeitigen Arbeit eigentlich ständig irgendwas an der API geändert werden.

Ggf. arbeiten wir in anderen Gebieten?
Wenn die API für die Verwendung nach außen für externe Systeme gedacht ist, kann ich mir sehr gut vorstellen, dass sie sich nur selten ändert.
Wenn die API aber für die Kommunikation zwischen zwei zusammen (im selben Team) entwickelten Projekten (Client-Server) gedacht ist, kann das wieder anders aussehen.
Bei uns dient die REST-API hauptsächlich zur Kommunikation zwischen dem Web-Frontend und dem ASP.NET-Backend sowie zu anderen intern entwickelten System und bei der derzeitigen Auftragslage ist da ziemlich viel Bewegung.

Tatsächlich arbeiten wir aktuell mit einem Source-Generator, der Client-Code auf Basis der Controller-Definitionen generiert, allerdings ist der ziemlich komplex und bringt auch ein paar Nachteile mit sich.

Daher würd eich diese einfach händisch coden -- aus Kosten/Nutzen-Sicht.

Ja das stimmt natürlich, aus Sicht einer Firma wäre das für ein Projekt definitiv besser. Für viele Projekte könnte das aber wieder anders aussehen.
Und ich schreibe hier gar nicht für meine Firma, das ganze ist meine private Idee und würde dann auch auf NuGet landen, sodass man in den Projekten gar keinen Aufwand mehr hat - im Idealfall natürlich.

13.03.2024 - 13:21 Uhr

Natürlich, man kann es auch "zu Fuß" machen, muss das dann aber auch immer bei jeder Änderung tun.
Und dadurch gäbe es auch eine ganze Menge Overhead:

  • Das Interface muss gepflegt werden
  • Netzwerk-Implementierung muss gepflegt werden
  • InProcess-Implementierung muss gepflegt werden
  • z.B. DI-Scope muss verwaltet werden

Und ich kann für die InProcess-Implementierung nicht einfach nur das Interface im Server-Service implementieren, da die Methoden-Signaturen völlig anders sind.

Man könnte natürlich eine weitere Abstraktion z.B. mit dem Mediator-Pattern dazwischen schieben, da würde das etwas einfacher werden, aber es gibt immer noch mehr Aufwand, der für jede Änderung anfällt.

Mit meiner Idee wäre das alles nicht nötig, man hat genau den Aufwand, den man bei gRPC sowieso immer hat und auch braucht, aber darüber hinaus nicht mehr.

Oder übersehe ich etwas?

13.03.2024 - 01:32 Uhr

'n Abend,

ich versuche mich gerade an den GitHub Actions (meine Erfahrung hält sich also in Grenzen ) und möchte für ein VSIX Projekt einen Workflow erstellen, der automatisch baut und einen Release inkl. Tag erstellt.

Da es ein altes VSIX Projekt ist, muss das alles auf Windows 2019 laufen und alle bereits vorhandenen Actions, die ich gefunden habe, liefen nur unter Linux, die einzige Option, die ich sehe, ist also die git cli.

Das ist mein Test-Workflow:

name: Test

on:
  workflow_dispatch:

jobs:
  test:
    runs-on: windows-2019
    steps:
      - uses: actions/checkout@v4.1.1
        with:
          fetch-depth: 0
      
      - name: Create version tag
        run: |
          $url = git config --get remote.origin.url
          $url = $url -replace "https://", "https://${{ secrets.GITHUB_TOKEN }}@"
          git remote set-url origin $url
          git config user.name "GitHub Action"
          git config user.email "<>"
          git tag run${{ github.run_number }}
          git push origin run${{ github.run_number }}

Mein Problem ist, dass ihm die Berechtigungen fehlen:

remote: Write access to repository not granted.

fatal: unable to access 'https://meine/schöne/url: The requested URL returned error: 403

Das oben ist einer meiner Versuche.
Ansonsten habe ich auch versucht:

  • git config credential.helper '!f() { sleep 1; echo "username=git token=${{ secrets.GITHUB_TOKEN }}"; }; f'
  • git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.GITHUB_TOKEN }}" push origin run${{ github.run_number }}

Beides habe ich online gefunden und einfach nur Just4Fun ausprobiert

Hat jemand eine Idee, wie ich das ans Laufen bekomme?

Besten Dank für die Hilfe 😃

12.03.2024 - 18:15 Uhr

Z.B. können die vom gRPC-Tooling erstellten Services auch ohne gRPC aufgerufen werden.

Genau das meine ich

Ich versuche es mal mit etwas Code zu verdeutlichen:

// Netzwerk - so wie man gRPC normalerweise benutzt
using var netChannel = GrpcChannel.ForAddress("https://localhost:1234");
await TestAsync(new TestService.TestServiceClient(netChannel));

// InProcess - meine Idee
await using var services = new ServiceCollection()
    .AddInProcessGrpcChannel(b => b
        .WithService(InProcessServiceInfo.Create<TestServiceImpl>())
        .UseScopedServiceProvider())
    .BuildServiceProvider();

var inpChannel = services.GetRequiredService<InProcessGrpcChannel>();

await TestAsync(new TestService.TestServiceClient(inpChannel));

// TestAsync()
static async Task TestAsync(TestService.TestServiceClient client)
{
    // Der Code hier weiß nicht, ob die Kommunikation über das Netzwerk oder im selben Prozess stattfindet.
}

Der entscheidende Faktor ist die TestAsync-Methode, die für beides identisch ist.

Mein InProcess-Channel (bzw. der Invoker) sucht in seiner Liste von bekannten Services nach dem richtigen Service und der Methode und ruft beides auf. Es findet keine Netzwerk-Kommunikation statt, keine Serialisierung, kein Protokoll, es wird nur die Methode direkt aufgerufen.

Das, was ich von gRPC nutze, ist den vom gRPC-Tooling erstellte Code als Abstraktion, um die Services/Methoden zu suchen, zu instanziieren (ggf. in einem Scope) und aufzurufen.

11.03.2024 - 14:05 Uhr

Ich glaube, Du hast mich falsch verstanden.
Ich nutze gRPC im Wesentlichen nur als Kommunikations-Layer, es wird nichts serialisiert und es gibt kaum zusätzlichen Overhead.
Die aus den proto-Dateien generierten Typen bieten mir aber alles, was ich brauche, um die Services und Methoden zu finden und genau das ist es, was ich nutze.

Effektiv ist mein Idee (bzw. mein erster Entwurf) also nur ein Quasi-Ersatz für die gRPC-Kommunikation hinter der generierten Abstraktion. Das Protokoll dahinter gibt es also gar nicht mehr, es gibt nur noch die Abstraktion der offiziellen .NET-Implementierung.

Aber klar, wenn man nur im selben Prozess kommunizieren will, ist das ganze überflüssig und unnötig. Aber es geht ja gerade um die Projekte, wo man beides haben möchte, abhängig von der Konfiguration.

10.03.2024 - 22:04 Uhr

Hallo zusammen,

ich habe mich mit gRPC beschäftigt und dabei über eine Idee nachgedacht:
Ist ein nahtloser Wechsel zwischen Client-Server und Monolith ohne großem zusätzlichen Aufwand möglich?

Die Idee ist, eine Channel-Implementierung zu entwickeln, die in der Lage ist, Client-Aufrufe im selben Prozess zu verarbeiten, möglicherweise mit einem DI-Scope. Je nach Anwendungs-Konfiguration wird dann der originale Channel oder der InProcess-Channel verwendet.

Die Anwendung wäre weiterhin strikt in zwei Bereiche unterteilt: Client und Server. Der Client kommuniziert mit dem für gRPC generierten Client und je nach Konfiguration läuft die Kommunikation entweder über das Netzwerk, oder im selben Prozess.

Diese Idee richtet sich natürlich an eine sehr spezifische Zielgruppe, da sie nur für Anwendungen sinnvoll ist, die diese Art des Wechsels benötigen. Denkbar wäre z.B. eine Anwendung, die entweder als eigenständige Desktop-Anwendung oder im Team mit einem zentralen Server genutzt werden soll. Bei einer Desktop-Anwendung möchte man natürlich keinen separaten Server starten müssen.

Für Tests könnte es auch nützlich sein, ich halte es aber für besser, die Business-Logik vernünftig zu abstrahieren und dann direkt ohne gRPC zu testen. Außerdem kann ein InProcess-Channel niemals das Verhalten einer echten Netzwerk-Kommunikation vollständig abbilden.


Warum ich nun dieses Thema auf mache:

Was denkt ihr über diese Idee?
Welche Funktionen haltet ihr für wichtig?
Welche Risiken sollten explizit getestet werden?

Besten Dank 😃

21.01.2024 - 19:11 Uhr

Zitat von Peter Bucher

Wenn man eine private Nachricht schreiben möchte - im Darkmode! - ist die Schrift weiss auf weissem Hintergrund, man sieht das geschriebene also leider nicht.

Schau dir mal die Browser-Extension "Dark Reader" an - hat mir Abt mal empfohlen.
Ist natürlich nicht so gut, wie das Original-DarkTheme, aber es funktioniert ganz gut.

12.01.2024 - 14:51 Uhr

Entweder CSV mit leeren Zellen, oder Du erstellst eine xlsx-Datei, z.B. damit:
https://github.com/ClosedXML/ClosedXML oder https://github.com/ClosedXML/ClosedXML.Report

CSV ist einfacher, XLSX erlaubt natürlich sehr viel mehr Anpassungen.

09.01.2024 - 18:58 Uhr

Und wenn Du es stattdessen aus dem CKEditor raus in ein Panel rechts daneben über/unter dem Tipps-Panel legst?
Da könntest Du dann auch noch andere nützliche Tools unterbringen.

09.01.2024 - 16:29 Uhr

Es gibt ja zwei Ansichten:

Die "gerederte" Ansicht und die "Quellcode"-Ansicht vom Markdown.
In der "gerenderten" Ansicht kannst Du ganz normal "List<T>" schreiben, im Markdown wird das dann escaped.
Wenn Du aber im Markdown-Quellcode "List<T>" schreibst, dann musst Du auch selber escapen, dafür musst Du vor dem "<" ein Slash schreiben: "List\<T>"

Aus meiner Sicht funktioniert also alles korrekt.


Ich hab gerade den "Links"-Dropdown gefunden - schön, dass es das jetzt gibt 😃
Aber kannst Du da noch eine Schnell-Suche im Dropdown einbauen? So ist das etwas unpraktisch und die normale Forensuche ist vermutlich immer noch schneller.

31.12.2023 - 15:10 Uhr

Stimmt, daran hatte ich gar nicht gedacht - einfach Brushes nehmen 😄

cmbFontColors.ItemsSource = typeof(Brushes).GetProperties()
    .Select(p => new BrushInfo()
    {
        Name = p.Name,
        Brush = (Brush)p.GetValue(null)
    });
31.12.2023 - 12:53 Uhr

wenn ich einen Default setzen möchte

Die ComboBox hat einen SelectedValuePath-Property, kann sein, dass die dein Problem löst, wenn Du dort "Name" einträgst.
Ich bin mir aber nicht sicher, was dann in e.AddedItems steht, kann sein, dass da dann auch der Name steht, das müsstest Du prüfen.

Oder Du nutzt Folgendes:

var defaultItem = typeof(Colors).GetProperty(nameof(Colors.Blue));

Das liefert dir das selbe (ganz wichtig) PropertyInfo-Objekt, sodass Du das einfach als ausgewähltes Item in der ComboBox nutzen kannst.

ob ich in zwei, drei Jahren direkt Blicke, warum da kein Brush drinne ist

Das typeof(Colors) ist nicht die Colors-Klasse, sondern ein Type-Objekt der Colors-Klasse, was Typ-Informationen bereithält - ganz wichtiger Unterschied ^^

Die Type-Klasse ist dafür gedacht, Metadaten aller Typen (Klassen, Structs, Delegates, Generics, etc.) verfügbar zu machen und genau das tut sie. Und die GetProperties- und GetProperty-Methoden liefern dir das Gegenstück für eine einzelne Properties. Mit dem Type-Objekt kannst Du eine Instanz des Typs erzeugen (bzw. mit der Activator-Klasse) und das PropertyInfo-Objekt kann den Wert abrufen oder setzen - vorausgesetzt der Typ bzw. die Property lassen das zu. Aber es bleiben Metadaten, nicht mehr und nicht weniger.

31.12.2023 - 01:27 Uhr

Das ursprüngliches Problem ist (war), wie Du schon vermutet hast, folgende Zeile:

cmbFontColors.ItemsSource = typeof(Colors).GetProperties();

Die GetProperties-Methode gibt dir nicht die einzelnen Farben (Color-Werte) zurück, sondern PropertyInfo-Objekte, die Metadaten zur jeweiligen Property enthalten. Konkret bekommst Du RuntimePropertyInfo-Objekte zurück, das ist eine Ableitung von PropertyInfo und wird von .NET intern benutzt. Diese Metadaten sind dann z.B. der Typ oder der Name der Property. Recherchier mal zu Reflection, dann wird es vielleicht klarer.

Dein Layout (das {Binding Name}) funktioniert trotzdem, weil die PropertyInfo auch eine Name-Property hat. Microsoft hat in diesem Fall die ganzen Properties in der Colors-Klasse so benannt, wie auch die Farben heißen, deshalb wird der richtige Farb-Name dargestellt.

Und deine vorläufige Lösung funktioniert, weil WPF einen ValueConverter für die ForegroundProperty hat, der automatisch in einen Brush konvertieren kann. Das ist nötig, damit Du im XAML einfach nur den Namen der Farbe eintragen kannst, der ValueConverter sucht diesen Namen dann in den bekannten Farben und konvertiert automatisch. Und dass die Namen der Properties in der Colors-Klasse identisch zu den bekannten Farb-Namen sind, hatte ich ja schon erwähnt.


Probier mal folgendes:

cmbFontColors.ItemsSource = typeof(Colors).GetProperties().Select(p => p.GetValue(null));

Damit sollten die einzelnen Items dann vom Typ Color sein, dann kannst Du in der cmbFontColors_SelectionChanged-Methode auch darauf casten.

Allerdings wirst Du damit dein Ziel auch nicht erreichen, aus zwei Gründen:

  • Die ForegroundProperty ist nicht vom Typ Color, sondern vom Typ Brush. Der ValueConverter konvertiert aber nur String (Farb-Name) und Brush hin und her, nicht aber die Color-Werte. Gleiches gilt für die Fill-Property von Rectangle, das funktioniert dann auch nicht mehr.
  • Die Color-Werte haben keine Name-Property, dein Binding auf den Namen funktioniert also nicht.

Daher ist deine (vorläufige) Lösung sogar genau richtig so.

Du könntest aber auch einen Umweg über eine Zwischen-Klasse gehen. Dafür musst Du dann wie gehabt die PropertyInfo-Objekte abrufen und pro Item dann eine Instanz der Zwischen-Klasse erstellen:

cmbFontColors.ItemsSource = typeof(Colors).GetProperties()
    .Select(p => new ColorInfo()
    {
        Name = p.Name,
        Brush = new SolidColorBrush((Color)p.GetValue(null))
    });

Im XAML dann:

<Rectangle Fill="{Binding Brush}" Width="16" Height="16" Margin="0,2,5,2" />
<TextBlock Text="{Binding Name}"/>

Und in der cmbFontColors_SelectionChanged-Methode kannst Du dann auf deine Zwischen-Klasse casten und die Brush-Property verwenden:

ApplyPropertyValueToSelectedText(TextElement.ForegroundProperty, ((ColorInfo)e.AddedItems[0]).Brush);

Diese Lösung hätte den Vorteil, dass Du beliebig viele eigene Farben und die volle Bandbreite der Möglichkeiten der Brush-Klasse nutzen kannst, da gibt's nicht nur SolidColorBrush. Oder Du möchtest die Farben lokalisiert darstellen, dabei würde dir diese Zwischen-Klasse auch in die Hände spielen.

Welche Lösung jetzt besser ist, da kann man sicher hervorragend drüber streiten und hängt auch von deinen Anforderungen ab.
Bedenke aber auch, dass deine Lösung nur dann verständlich ist, wenn man weiß, wie WPF bei dem Thema tickt, das hast Du ja auch schon gemerkt.
Insofern ist der Weg über die Zwischen-Klasse definitiv leichter verständlich.


Abgesehen davon:

Ein try mit leerem catch macht man nicht, das würde ich dir beim Code-Review um die Ohren hauen 😉
Das macht man nur, wenn man auch einen sehr guten Grund dafür hat und dieser Grund gehört dann auch kommentiert.


@Th69:

Zitat von Th69

Außerdem wird dort TextBlock.ForegroundProperty verwendet.

Das ist identisch. In der TextBlock-Klasse wird TextElement.ForegroundProperty.AddOwner(typeof(TextBlock)) verwendet, was wiederum this zurück gibt, siehe hier und hier.

06.12.2023 - 09:53 Uhr

Wenn das alles so doof ist, frage ich mich, warum das dann in C# eingebaut wird?

Es ist nicht doof, aber man muss es - wie Abt schreibt - überlegt verwenden.

Microsoft baut viele Funktionen ein, wie Du sie benutzt, ist deine Sache.
Und sie haben auch eine Funktion eingebaut, dass man vor dem ganzen Code auf null prüfen und dann ein Default-Verhalten implementieren kann, das macht die ganzen null-Checks danach überflüssig.
Der Null-conditional Operator lohnt sich besonders dann, wenn man nur eine Null-Prüfung braucht, dann kann man es kompakter schreiben, aber das ist hier nicht der Fall, hier setzt sich das ganze fort.

if (collection is null)
    return;

var enumerator = collection.GetEnumerator ();
enumerator.Reset();

while ( enumerator.MoveNext() )
{
    var value = enumerator.Current;
    ...
}

By the way: Das Reset() braucht man nach dem GetEnumerator()-Aufruf nicht.
Nur wenn man erneut iterieren möchte, braucht man Reset(), was ich persönlich bisher noch nicht hatte.

04.12.2023 - 09:03 Uhr

Zu .NET Framework, .NET Standard, .NET Core und .NET >5 findest Du hier einen ausführlichen Artikel von Abt.
Und ja, das neue .NET ist plattformunabhängig, läuft also auch auf den von dir genannten Betriebssystemen.

Und wenn es dir um die Zukunft geht und Du das alte .NET Framework nicht unterstützen musst, dann nimm das aktuellste .NET, beachte dabei aber die Support Policy.
Wenn Du auch das alte .NET Framework unterstützen musst (viele Frameworks müssen/wollen das), ist .NET Standard die einfachste, aber nicht die einzige Möglichkeit. Du kannst alternativ auch mehrere .NET Versionen unterstützen.

Wenn es dir um Desktop- und Mobile-Anwendungen geht, sieht das ganze aber komplizierter aus.

Von Microsoft gibt es MAUI, das ist prinzipiell plattformunabhängig, läuft aber (noch) nicht auf Linux.
Alternativ kannst Du auch vollständig auf Web setzen und z.B. mit Blazor eine Web-Anwendung entwickeln, die Du dann in verschiedenen Desktop-Clients darstellen kann, das braucht auch nicht unbedingt einen Web-Server, mit MAUI Hybrid läuft Blazor auch als reine Desktop-Anwendung.

Außerhalb der Microsoft-Welt kenne ich noch:

  • Avalonia
  • UNO
  • Electron (Web)
  • Photino (Web)

Das vermutlich flexibelste wäre, Du setzt auf Blazor und suchst dann nach Clients, die das unterstützen, damit hast Du die ganze Bandbreite von Web-Technologien zur Verfügung. Umgekehrt bist Du aber eingeschränkt, wenn es um reine Desktop-Technologien geht, da musst Du dann auf die Möglichkeiten des jeweiligen verwendeten Clients zurückgreifen.

Oder Du willst kein Web, dann kannst Du zusätzlich noch WPF nutzen, das läuft zwar nur auf Windows, wird aber immer noch unterstützt.

Ich persönlich habe für ein privates Projekt Photino (für Blazor) rausgepickt, da es schlanker ist, als Electron und .NET 8 auch ohne Dirty-Fix unterstützt.

28.11.2023 - 18:07 Uhr

Als Tipp am Rande:

In der DbContext-Ableitung kann man in der OnConfiguring-Methode (oder wenn man die DbContextOptions baut) einen Interceptor registrieren:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.AddInterceptors(new EnableForeignKeysConnectionInterceptor());
}

Und die Klasse dazu:

internal sealed class EnableForeignKeysConnectionInterceptor : DbConnectionInterceptor
{
    public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData)
    {
        // PRAGMA foreign_keys = ON
    }
    public override async Task ConnectionOpenedAsync(DbConnection connection, ConnectionEndEventData eventData, CancellationToken cancellationToken)
    {
        // PRAGMA foreign_keys = ON
    }
}

Dann passiert das bei jeder Connection automatisch.

13.11.2023 - 19:15 Uhr

Ich habe soeben Version 0.3.0 hochgeladen:

v0.3.0 - 2023-11-13

Hinzugefügt

  • PackScan.Tool-Parameter, um das TargetFramework für das Projekt anzugeben.
  • PackScan.Tool-Parameter, um den RuntimeIdentifier für das Projekt anzugeben.
  • Funktion, um die maximale Größe für heruntergeladene Icons im Format "<Breite>x<Höhe>" festzulegen.

v0.2.1 - 2023-11-06

Behoben

  • Ein Fehler wurde behoben, der Probleme mit spezifischen Plattformversionen verursachte, wie sie in MAUI-Projekten verwendet werden.

v0.2.0 - 2023-11-01

Hinzugefügt

  • Neue Konfigurationsoption, die es ermöglicht, Inhalte nur aus Dateien zu laden, wenn keine URL angegeben ist und umgekehrt.
01.11.2023 - 00:05 Uhr

Ich habe soeben die erste Version hochgeladen.

30.10.2023 - 16:06 Uhr

Danke für das Lob 😃

Angenommen ich würde das nun in mehrere CI-CD Pipelines einbauen, wie hast Du Dir gedacht, dass das Setup zentral gemanaged werden könnte?

Das Projekt bietet ja einen SourceGenerator oder ein .NET Tool an, beide können MSBuild-Eigenschaften lesen. Das .NET Tool bietet zusätzlich noch eigene Parameter an, die dann die MSBuild-Eigenschaften überschreiben.

Du könntest deine zentrale Verwaltung der Einstellungen also mit Hilfe einer props-Datei lösen und in jedem Projekt einbinden.

Wenn die mehreren Projekte aber zusammengehören, solltest Du aber nur für das Start-Projekt der Code generiert lassen, da dieses Projekt als einziges alle Referenzen hat. Das Ergebnis kann man dann über Dependency Injection verteilen und dort nutzen, wo man es tatsächlich braucht.

29.10.2023 - 23:33 Uhr

Hallo zusammen,

Ich möchte Euch mein Projekt PackScan vorstellen, das die Handhabung von NuGet-Paketen erleichtert.

Kurz gesagt, PackScan ist ein Code-Generator, der die project.assets.json ausliest und sowohl die darin enthaltenen Daten als auch die Informationen aus den NuGet-Paketen selbst in Code umwandelt. Darüber hinaus kann es konfiguriert werden, um Texte und Icons aus dem Internet zu holen und in den Code zu integrieren. Dies erleichtert es, alle genutzten Pakete und Lizenzen im Blick zu halten und auf anschauliche Weise in der Anwendung darzustellen – eine Anforderung, die in früheren Projekten von mir aufkam.

Hier die Highlights:

  • Automatische Sammlung von Informationen der verwendeten Pakete.
  • Analyse der Lizenzen und Warnungen bei unerwünschten Lizenzen.
  • Verfügbar als .NET Tool und Source-Generator für MSBuild.
  • Umfangreiche Konfigurationsmöglichkeiten für individuelle Anpassungen und Performance-Optimierung.

Ich freue mich auf eure Meinungen, Vorschläge und Kritik! Bitte hinterlasst eure Kommentare hier oder auf der GitHub-Seite des Projekts.

Beste Grüße

23.10.2023 - 20:00 Uhr

Hilft vielleicht:

Uninstall or remove Visual Studio
Dort gibt's einen Bereich "Remove all with InstallCleanup.exe"

06.10.2023 - 16:19 Uhr

Zum Lesen von Excel such mal nach "ClosedXML". InterOp brauchst Du mit dem OpenXML-Format nicht, aber ohne NuGet-Packages geht's nicht.

Wie wäre es, wenn Du eine DLL programmierst, als Datei irgendwo hin legst, in dem Script die Datei lädst und mit Reflection irgendeine Methode ausführst?

22.09.2023 - 09:11 Uhr

So funktioniert das nicht.
Was Du da unbewusst nutzt, heißt Pattern-Matching.

Diese Switch-Schreibweise wird vom Compiler in "typeof(T) is IClassItemMethodArgumentAttribute" (und so weiter) aufgelöst und das "is" prüft, ob die Instanz links dem Typ rechts zugeordnet werden kann. In deinem Fall heißt das, dass Du prüfst, ob das RuntimeType-Objekt (das wird von "typeof) zurückgegeben) das jeweilige Interface implementiert, was natürlich nie der Fall ist.

Deine Prüfung musst Du manuell mit Reflection vornehmen, Du brauchst die Type.IsAssignableFrom(Type) Methode.

13.09.2023 - 14:14 Uhr

"Key" ist ein Enum

if (e.Key == Key.D4)
{
}
22.08.2023 - 13:16 Uhr

Zitat von Abt

Es ist absolut valide (und notwendig) dass Handler andere Handler aufrufen.

Hierzu hatte ich vor einer Weile eine gegenteilige Aussage gelesen - Quelle finde ich nicht mehr.

Verstehe ich das richtig, dass ein Handler auch andere Handler aufrufen darf, also eine theoretisch beliebig lange Kette von Handlern entstehen kann?
Mit MediatR würde also ein RequestHandler eine IMediator-Dependency erhalten und andere Requests absenden.

Das ist ein Detail, was mir auch noch unklar ist:
Was ist, wenn mehrere Handler teilweise die gleiche oder ähnliche Logik haben?
Es wäre einfach, das alles in einen weiteren Handler auszulagern und den aufzurufen, aber ist das der beste Weg?

21.08.2023 - 14:45 Uhr

Wozu auch immer sie ein Repository haben wollten, ich hätte gedacht, der Code reicht 😄
Naja, jedenfalls gibt's jetzt ein Repository mit dem Bug.

Hatte zwar auch mal im Source geschaut, bin aber leider damit nicht sonderlich vertraut.

Dito - ich werde aus dem Code auch nicht schlau, zumindest sehe ich keinen Grund, warum er IRootObjectProvider nicht finden sollte, alles andere aber schon.

19.08.2023 - 20:55 Uhr

Ne, hilft leider nicht.

Ich brauch den IRootObjectProvider ja abhängig vom Kontext der MarkupExtension.
Die Services der Application sind dagegen ohne Kontext und enthalten auch keinen IRootObjectProvider.

Ich habe auch keine Lösung für das Problem gefunden, ist scheinbar ein Bug in MAUI.
Ich habe aber einen Workaround gefunden, sodass ich zumindest weiterarbeiten kann.

18.08.2023 - 18:07 Uhr

Hallo zusammen,

ich hab das Problem, dass mein MAUI-Projekt nach dem Publish sich mit einem EventLog-Eintrag sofort wieder beendet.

Das Problem ist Folgendes:
Ich habe eine MarkupExtension, die anhand des IServiceProvider-Parameters den IRootObjectProvider-Dienst abruft.
Genau diesen Dienst scheint es im Relase-Mode aber nicht zu geben? Im Debug-Mode ist alles korrekt da.

Das ist der Code der ServiceProvider-Klasse, die da zum Einsatz kommt:
https://github.com/dotnet/maui/blob/main/src/Controls/src/Xaml/XamlServiceProvider.cs
Der Code ergibt für mich keinen Sinn, wenn es den IRootObjectProvider nicht gibt, müsste der context null sein, aber dann gäbe es auch andere Dienste nicht, die aber da sind.

Hat jemand eine Idee, woran das liegt und wie ich es beheben kann?

Besten Dank 😃

04.08.2023 - 14:15 Uhr

Zitat von gfoidl

Aber von vornhinein würde ich (heute) keine Projekte mehr zwangsmäßig in (thematische) Assemblies aufteilen (XYZ.View, XYZ.ViewModel, XYZ.Models, etc.), sondern das nach Anwendungsfällen (use cases), Benutzeraktionen, usw. aufteilen.

Kannst Du das näher erleutern, woran Du das normalerweise orientierst?
Z.B. Feature, Modul, Dialog getrieben, etc.?

30.07.2023 - 15:41 Uhr

Probier's aus, ob das von Haus aus geht, weiß ich nicht, ich rate mal Nein.
Aber sowohl in Newtonsoft.Json, als auch System.Text.Json kann man Converter bauen.
Ich würde das auch tun, einfach um den Color-Type verwenden zu können.

30.07.2023 - 15:32 Uhr

Beide Varianten sind suboptimal, Variante 1 ist aber ein bisschen weniger suboptimal, als Variante 2. 😉
So spontan fällt mir kein einziger sinnvoller Fall ein, wo ein Methoden-Ergebnis als Property bereitgestellt wurde.

Richtig wäre:

public class Test
{
    private int _ctorArg;
    
    // Wenn Du den Wert public brauchst, dann eine Property:
    // public int CtorArg { get; }

    public Test(int arg)
    {
         _ctorArg = arg;
    }

    public int CalculateSomething(int arg)
    {
        return _ctorArg * arg;
    }
}

Ist natürlich schwer zu beurteilen, wie man etwas macht, ohne das Ziel zu kennen.
Es gibt viele verschiedene Wege, das selbe Ziel zu erreichen, doch normalerweise sollte man diesen Weg wählen (außer man braucht etwas anderes), da es am einfachsten ist und am wenigsten fehleranfällig.

(Und natürlich sollten bessere Namen gewählt werden)

30.07.2023 - 14:03 Uhr

Woher soll der geneigte Programmierer wissen, welche Objekte aus einer UI-Klasse stammen?

DispatcherObject ist das magische Stichwort, alle die Klassen, die von DispatcherObject ableiten, dürfen nur im UI-Thread verwendet werden.

aber ich werde die PK-Klasse auf String-Brushes (Stringwert, der ein Color-Wert darstellt) umstellen

Nimm doch direkt die Color-Werte? Ist etwas intuitiver, man sieht am Typ, was es ist.

Und den BrushConverter hast Du schon, Du musst ihn nur noch so umstellen, dass es ein ValueConverter ist (IValueConverter) und dann im Binding benutzen. Du bindest dann die String/Color-Property, als Converter deinen ColorToBrushConverter und siehe da, es funktioniert 😉

... zumindest solange es kein anderes Problem gibt.

30.07.2023 - 09:30 Uhr

Wenn Du dich mit WPF beschäftigst, sollte eigentlich kein Weg an MVVM vorbei führen.

Aber wenn Du noch so sehr Anfänger bist, solltest Du eher mit der Konsole anfangen.
WPF mag auf den ersten Blick zwar einfach wirken, aber an vielen Stellen gibt's dann doch Komplexitäten (wie z.B. MVVM), die einen Anfänger überfordern können.

Mit den Grundlagen, der OOP und den häufigen Sprach-Funktionen solltest Du mindestens sattelfest sein, bevor Du mit WPF startest.

30.07.2023 - 09:15 Uhr

@Th69

Tatsache, Du hast Recht, ich hab's gerade nochmal getestet.

Dass ein Brush ein UI-Objekt ist, wusste ich, aber ich dachte, das Ding tut solange nichts, bis es irgendwo benutzt wird.
Laut meinem Test ist das auch der Fall, man kann das Brush-Objekt im anderen Thread erstellen und damit arbeiten, aber in der UI nutzen darf man es danach nicht, selbst wenn das wieder im UI-Thread stattfindet. Deshalb knallt es auch erst bei der PK-Zuweisung, wo er eigentlich im richtigen Thread ist.
Und wenn man es Freezed, tritt das Problem nicht mehr auf.

Ist ein gutes Beispiel, warum man View und ViewModel nicht mischen sollte 😉


Mein kleines Test-Projekt:

MainWindow.xaml

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <StackPanel>
        <Button Click="Button_Click" Content="Test" HorizontalAlignment="Left" />
    </StackPanel>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
        var brush = await Task.Run(CreateBrush);
        Background = brush;
    }

    private Brush CreateBrush()
    {
        var brush = new SolidColorBrush()
        {
            Color = Colors.Red,
        };

        //brush.Freeze();

        return brush;
    }
}
30.07.2023 - 08:55 Uhr

Das mit dem Binding hatte ich aber erwähnt 😉
Das ist aber auch nicht der Fehler, dass er da eine Exception wirft, ist korrekt so, weil er aus einem anderen Thread kommt.
Du hast also irgendwas, was dafür sorgt, dass er aus einem anderen Thread kommt und das gilt es zu finden.
Im hier gezeigten Code sehe ich davon aber nichts und der Debug.WriteLine-Test hat das auch bestätigt.

Wie genau das CanFreeze da hinein spielt, weiß ich nicht, ich vermute aber, dass das eine Optimierung ist, damit WPF die Farben nicht auch noch überwachen muss. Dann ändert sich die Farbe nicht, also muss auch im UI-Thread nichts aktualisiert werden.

29.07.2023 - 21:53 Uhr

Ich meine sowas:

vm.PropertyChanged += MyOnPropertyChangedHandler;

// ...

private void MyOnPropertyChangedHandler(object? sender, PropertyChangedEventArgs e)
{
    // ...
}
29.07.2023 - 21:04 Uhr

Die Ausgabe ist wie erwartet korrekt - hätte mich auch gewundert, aber sicher ist sicher.

Mit EventHandler meine ich aber nicht das OnChanged, sondern ob Du irgendwo das PropertyChanged Event selber abonierst und darauf reagierst. Wäre möglich, dass der Fehler ganz woanders liegt und nur indirekt durch die PK-Zuweisung ausgelöst wird.

29.07.2023 - 17:41 Uhr

Schreib mal zwischen jedes await in der AssociatedObject_Loaded ein Debug.WriteLine($"{Environment.CurrentManagedThreadId} | {SynchronizationContext.Current?.GetType()?.Name}")
Das sollte eigentlich immer die gleiche ID sein, aber da es bei der PK-Zuweisung zu der Exception kommt, deutet es darauf hin, dass es dabei eben nicht die richtige ID ist.
Und der SynchronizationContext sollte nicht null sondern DispatcherSynchronizationContext sein, da der für die Synchronisation zuständig ist.

Oder das PropertyChanged-Event tut mehr, als wir wissen. In den Kommentaren von der MVVM_Base steht etwas komisches über den Dispatcher. Hast Du irgendwo eigene PropertyChanged-EventHandler geschrieben, ggf. in der MVVM_Base.

29.07.2023 - 15:30 Uhr

Deine Benennungen sind nicht sehr intuitiv. Was ist PK?

Nutzt das irgendwelche Events, INotifyPropertyChanged?
Du übergibst das an ProgrammDaten_LoadPK2 in einen anderen Thread und änderst Dinge.

Warum es erst danach knallt, sehe ich so auf Anhieb nicht, aber die genannten Zugriffe sehen ziemlich falsch aus.

29.07.2023 - 13:37 Uhr

Wo genau tritt die Exception auf?
Wenn er dir nur ein await anmeckert, dann geh mal in die Methode und debugge dort, um zu sehen, wo er raus springt.

Ich rate mal, Du machst in ProgrammDaten_LoadPK2 irgendetwas, was da nicht gemacht werden darf, denn genau das läuft auf einem ThreadPool-Thread und darf nicht auf den UI-Thread zugreifen. Das kann auch ein Event sein, oder eine ViewModel-Property, die Du änderst, wenn die UI irgendwie darauf reagiert, führt das zu der Exception.

29.07.2023 - 13:31 Uhr

Die Übergabe eines Externen Wertes als Methodenparameter an die Methode eines Objektes wäre also ok ?

Ich weiß nicht, was Du unter "externen Wert" verstehst.

Wenn Methode A eine andere Methode B aufruft, dann darf sie natürlich jeden Wert, der benötigt wird, als Parameter übergeben, dafür sind sie da.

29.07.2023 - 11:01 Uhr

Du darfst auch keine UI-Controls außerhalb des UI-Threads anfassen. Das gilt auch für ViewModel-Eigenschaften und Events, wenn die UI daran bindet bzw. darauf horcht.

Dafür gibt's ja (unter Anderem) async/await. Wenn Du vor dem await im UI-Thread warst, bist Du es hinterher wieder, selbst wenn dazwischen ein anderer Thread am arbeiten war - einfach ausgedrückt. Das setzt aber voraus, dass Du vor dem await auch im UI-Thread warst.

Was sich nun wo und wie und aus welchem Thread aufruft, wissen wir nicht, das zeigst Du nicht.
Aber der Hintergrund-Thread darf nichts an der UI ändern.
Wenn Du aus dem Hintergrund-Thread live die UI aktualisieren willst, musst Du das über Umwege machen, z.B. indem Du aus der UI regelmäßig nach Aktualisierungen fragst. Es gibt aber auch Möglichkeiten, die Synchronisierung selber zu nutzen, entweder direkt über den SynchronizationContext (nicht nutzen, bevor Du weißt, was Du tust), oder Du guckst dir die JoinableTaskFactory-Klasse aus dem NuGet-Package "Microsoft.VisualStudio.Threading" an, dazu gibt's auch noch ein Analyzer-Package, was dich auf async/await-Fehler hinweisen kann - solltest Du auch installieren.

29.07.2023 - 00:49 Uhr

Frage 1:

Wann weise ich den Klassenvariablen Werte per Konstuktor zu bei der Objektinstantiierung mittels

Wenn Du es brauchst, der Wert also vor "Lebensbeginn" des Objektes bzw. im Konstruktor benötigt wird.
Ich behandle das immer so: Alle Werte, die das Objekt benötigt, um grundlegend zu funktionieren, werden über den Konstruktor verlangt.

wann erst nach der Instantiierung z.B. mittels direkten Ansprechens nach der Variablen mittels

Nie, weil immer Properties genutzt werden sollten.
Wenn ich die Frage auf Properties beziehe:

Dann, wenn Du es brauchst und die Klasse damit umgehen kann.
Ich halte immer möglichst viel immutable, also möglichst kein Setter, demnach auch möglichst viel über den Konstruktor oder einer Kombination der beiden neuen Features "required" und "init".


Frage 2:

Wann wähle ich die Variante mit getter/setter wie oben bei "Input" und wann arbeite ich ohne getter und setter wie bei "_input" ?

Das heißt "Property".
Und alles, was public ist, ist eine Property.
Alles was private ist, darf auch eine Variable sein.
Nutz das, was Du brauchst.


Frage 3:

Methode und Methodenparameter.
Allerdings sollte die Methode das Ergebnis selber zurückgeben und nicht mit einer "_output"-Variable arbeiten.


Frage 4:

Die sind ein Thema, aber selten bzw. für besondere Situationen.
Für einen Anfänger sind sie aber wahrscheinlich irrelevant.

28.07.2023 - 09:39 Uhr

Task hat doch sowas wie .ContinueWith(). Schau dir das mal an.

Das ContinueWith() sollte man normalerweise nicht benutzen.

Mit einem await erreicht man das gleiche (Code ausführen nach Beendigung des Tasks), nur dass das await einen besser lesbaren Code ermöglicht.

Für mich sieht das eher nach einem Bedien-Fehler mit async/await aus.

PS:
Hier stand vorher eine andere Nachricht.
Die habe ich gelöscht, da ich meinen Vorredner falsch verstanden hatte.

27.07.2023 - 23:08 Uhr

Was Du da zeigst, ist genau richtig so.

public async Task LoadDataAsync()
{
    data1 = await LoadDataFromXML1Async();
    data2 = await LoadDataFromXML2Async(data1);
    data3 = await LoadDataFromXML2Async(data1);
    data4 = Vorgang1_BuildView(data1,data3);
}

Genau so macht man das - das komische BuildView mal ausgenommen 😉

Das BuildView am Ende passt deshalb nicht ins Bild, weil in WPF eigentlich keine View gebaut werden muss. Du baust stattdessen ViewModels und bindest die View daran, den Rest macht WPF automatisch. Wenn Du aber genau das meinst (also das Aufbauen von ViewModels auf Basis der Daten), dann ist das auch richtig so.

27.07.2023 - 12:45 Uhr

Lies dich ein, wie man richtig mit async/await/Tasks arbeitet, durch Trial&Error wirst Du nicht glücklich.

Und deinen bisherigen hier gezeigten Code musst Du weg werfen.

// Immer einen Task zurückgeben, außer Du weißt, was du tust
public async Task LoadDataAsync() // Das "Async" ist Konvention
{
    // Hier darfst Du normal auf Properties vom ViewModel zugreifen, hier bist Du noch im UI-Thread.
    
    _data = await LoadDataFromDatabaseAsync();
    
    // Hier darfst Du normal auf Properties vom ViewModel zugreifen, das "await" kümmert sich um die korrekte Thread-Synchronisation
}

Im Behavior:

// Für Event-Handler keinen Task zurückgeben - ist eine Ausnahme.
private async void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
    // ...
    await vm.LoadDataAsync();
}

Das await synchronisiert alles selbständig, Du brauchst dafür keinen Dispatcher.
Das funktioniert aber nur, wenn das await im UI-Thread stattfindet, daher muss das auch im Event passieren, weil die sind in der Regel im UI-Thread.
Der Inhalt der asynchronen Methode wird dann auf einem anderen Thread ausgeführt und nach dem Await wechselt die Methode wieder zum UI-Thread.

27.07.2023 - 11:31 Uhr

NICHT im Konstruktor 😉 Ist generell eine blöde Idee.

Am besten ist es, Du lädst aus der UI gesteuert die Daten, also z.B. durch einen Button-Klick (Command).
Du kannst es aber auch in einem UI-Event das ViewModel über den DataContext suchen und dort die LoadDataAsync-Methode aufrufen, das wäre auch noch MVVM konform.
Ich hatte das früher ganz gerne durch meine selbst gebaute Navigation gelöst, das ViewModel wird informiert, wenn es angezeigt wird und lädt dann die Daten. Der Aufbau verlangt aber eine gewisse Infrastruktur.