Also ich habe mal die Standard-ConsoleApp mit folgenden dotnet Befehlen auf Windows, macOS und Ubuntu erzeugen lassen:
dotnet publish ConsoleApp -r osx-x64 --sc true -o ./pub/osx-x64 -p:PublishSingleFile=true
dotnet publish ConsoleApp -r win-x64 --sc true -o ./pub/win-x64 -p:PublishSingleFile=true
und dabei kam folgendes heraus
lauffähig | Windows | macOS | Ubuntu |
---|---|---|---|
osx-x64 | NEIN | ja | ja |
win-x64 | ja | ja | ja |
Verwendete Solution im Anhang
Wenn dir OpenXML zu sperrig ist kannst du es auch mit ClosedXml versuchen. Das arbeitet im Hintergrund mit OpenXml, hat aber den Anspruch etwas freundlicher im Umgang zu sein.
Ich frage mich, wie du das sehen willst, denn du lässt die Form wo das passiert gar nicht anzeigen.
PS: Verwende für Code doch bitte die Code-Tags.
Zitat von CSharpNewbie2022
- Bei diesem Schichten Modell trennen wir GUI, Business Logic and Data Access. Das habe ich so adaptiert. Als Data Access wäre der Zugriff zur Datenbank und jegliche Art von Lesen und Beschreiben von Dateien. Aber wie sieht es ..
- mit Schnittstellen (Ethernet, Ethercat, SPI, RS232, ...)
- mit Webdiensten also Kommunikation zu Api s aus, gehören diese Elemente auch in den Data Access Layer?
Diese Dinger unterscheiden sich zu DB und Dateien Zugriff darin, dass mit einem externen Partner kommuniziert wird, also gehört das nun zur Business Logic oder zu dem Data Access Layer?
Alles was nicht in deiner Anwendung ist, ist außen/extern und du baust dir für deine Anwendung eine Schnittstelle um mit dieser Außenwelt zu kommunizieren. Du darfst das gerne unter dem Begriff Infrastructure zusammenfassen.
Zitat von CSharpNewbie2022
2. Ich nutze den DI Container von Microsoft für alle ViewModels und diverse Singleton Klassen aus der Business Logic, damit diese in der GUI Schicht zur Verfügung stehen. Aber kleine Klassen, die nur innerhalb einer Klasse verwendet werden, erstelle ich immernoch mit new. Ist das eher gegen das Konzept? Sollte jedes Object mit dem DI erstellt werden?
Mal eine vereinfachte Darstellung:
Das ViewModel ist abhängig von einem DatenService und dieser ist abhängig von einem DbContext also werden diese alle am DI-Container registriert und von diesem erzeugt.
Das ViewModel schickt jetzt einen Datensatz zum speichern an den DatenService und der schickt das an den DbContext weiter. Dieser Datensatz (ist eine Klasse) hat keinerlei Abhängigkeiten (Dependencies) zu irgendwas und hat somit nichts mit Dependency-Injection am Hut.
Zitat von Stefan68
Eine frage zum Thema MVP ist es in der Praxis üblich das im Model auch Forms-Objekte wie z.B. ListViewItems oder andere Forms-Controls erzeugt werden und an die Form übergeben werden?
Hier wird das gut beschrieben Model-View-Presenter in WinForms
Aber über OnKeyUp/OnKeyDown bekommst du doch alles was du benötigst. So wie ich das sehe werden diese Steuerzeichen übermittelt, als wenn du die über die Tastatur eingeben würdest (so hat man früher zu DOS Zeiten die Sonderzeichen bekommen):
ALT
down0
down0
up0
down0
up3
down3
up0
downALT
up0
upDamit wird RS übermittelt. Die anderen Zeichen analog. Damit solltest du jetzt deinen Barcode zusammenstellen können.
Zitat von Abt
Persönlich lösche ich aber auch immer diese Collection und fülle sie dann.
Die generelle Empfehlung ist in WPF, genau das eigentlich nicht zu tun, weil der Overhead von OnPropertyChanged,der Allocation und die Bindungsaktualisierung i.d.R. teurer ist als das Clear, das O(n) ist.
Er meinte damit "lösche den Inhalt" - sieht man auch wenn man seinen Code dazu anschaut 😉 Also genau so wie empfohlen.
Zitat von Abt
OnPropertyChanged ist der Framework-Mechanismus, wenn Du Properties hast, die einen "Dirty State" haben, der eigentlich optimalerweise zu vermeiden ist. Damit triggerst Du WPF an, dass die Referenz neu bekannt gemacht wird und die Value wird aktualisiert.
Er lässt sich bei den Typen vermeiden, die eine direkte Möglichkeit der Bindung haben; wie die ObservableCollection.
Diese Aussage verstehe ich nicht, denn ich hatte noch niemals Probleme damit so eine
ObservableCollection
neu zu erstellen.Doch hattest Du, denn die Bindung in diesem Moment ist nicht mehr vorhanden, sie zeigt auf die alte Referenz.
Du wirst sicherlich eben auch OnPropertyChanged triggern, um das zu lösen.
Also ich habe speziell diese "Technische Limitation" nicht verstanden, da es einfach nur eine observable Property braucht wie halt bei jeder Property, deren Wert sich ändern kann und diese Änderungs Benachrichtigung benötigt wird.
Hier von einem Problem zu sprechen?
Ich kann versichern, dass ich da genau kein Problem hatte/habe/haben werde.
Zitat von Abt
Technische Limitation: Deine ObservableCollection darf nicht neu erzeugt werden.
Diese Aussage verstehe ich nicht, denn ich hatte noch niemals Probleme damit so eine ObservableCollection
neu zu erstellen.
Kannst du das bitte einmal erläutern?
Du kannst es mit SoundPlayer
versuchen.
Der spielt z.B. eine WAV-Datei ab die du beliebig starten und anhalten kannst.
private readonly SoundPlayer _sound = new SoundPlayer("<Pfad zu einer WAV-Datei>");
const int DIT_LENGTH = 100;
const int DAH_LENGHT = DIT_LENGTH * 3;
const int SYMBOL_LENGTH = DIT_LENGTH;
const int CHAR_LENGth = DIT_LENGTH * 3;
const int WORD_LENGTH = DIT_LENGTH * 7;
public async Task SOS()
{
// S
await Dit();
await SymbolGap();
await Dit();
await SymbolGap();
await Dit();
await CharGap();
// O
await Dah();
await SymbolGap();
await Dah();
await SymbolGap();
await Dah();
await CharGap();
// S
await Dit();
await SymbolGap();
await Dit();
await SymbolGap();
await Dit();
}
public async Task Dit()
{
_sound.Play();
await Task.Delay(DIT_LENGTH);
_sound.Play();
}
public async Task Dah()
{
_sound.Play();
await Task.Delay(DAH_LENGTH);
_sound.Play();
}
public Task SymbolGap() => Task.Delay(SYMBOL_LENGTH);
public Task CharGap() => Task.Delay(CHAR_LENGTH);
public Task WordGap() => Task.Delay(WORD_LENGTH);
Die Antwort findest du auf stackoverflow.com.
Is so, liegt aber an Windows selber, dann dotnet leitet das nur an die Window Api weiter und das kommt dabei heraus.
Es wäre schön, wenn du Code in die entsprechenden Code-Tags packst, dann würde dein Code z.B. so aussehen (hübsch, gell?)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace Beep_Test
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void CmdEnde_Click(object sender, EventArgs e)
{
Close();
}
private void CmdStart_Click(object sender, EventArgs e)
{
Console.Beep(600, 100); // Länge 50 ms
Console.Beep(600, 100);
Console.Beep(600, 100);
Console.Beep(600, 300); // Länge 150 ms
Console.Beep(600, 300);
Console.Beep(600, 300);
Console.Beep(600, 100); // Länge 50 ms
Console.Beep(600, 100);
Console.Beep(600, 100);
}
}
}
// Create a builder by specifying the application and main window.
var builder = WpfApplication<App, MainWindow>.CreateBuilder(args);
// Configure dependency injection.
// Injecting MainWindowViewModel into MainWindow.
builder.Services.AddTransient<MainWindowViewModel>();
// Configure the settings.
// Injecting IOptions<MySettings> from appsetting.json.
builder.Services.Configure<MySettings>(builder.Configuration.GetSection("MySettings"));
// Configure logging.
// Using the diagnostic logging library Serilog.
builder.Host.UseSerilog((hostingContext, services, loggerConfiguration) => loggerConfiguration
.ReadFrom.Configuration(hostingContext.Configuration)
.Enrich.FromLogContext()
.WriteTo.Debug()
.WriteTo.File(
@"Logs\log.txt",
rollingInterval: RollingInterval.Day));
var app = builder.Build();
await app.RunAsync();
Zitat von Th69
Oder suche mal nach "high precision/resolution timer", z.B. PrecisionTimer.NET.
Das löst das Problem aber auch nicht, es wird evtl. etwas kleiner. Mein Ansatz löst das Problem und macht es sogar noch testbar.
Hier habe ich mal etwas, womit du ein wenig herumspielen kannst.
https://dotnetfiddle.net/QqcXHo
using System;
var startDateTime = new DateTimeOffset( 2021, 01, 01, 00, 00, 00, TimeSpan.Zero );
var endDateTime = startDateTime.AddMinutes( 1 );
var clock = new FakeClock { UtcNow = new DateTimeOffset( 2021, 01, 01, 00, 00, 00, TimeSpan.Zero ) };
var foo = new Foo( clock, TimeSpan.FromMilliseconds( 500 ) )
{
ActualSpeed = 60,
};
foo.Start();
while ( clock.UtcNow < endDateTime )
{
var timeSpan = TimeSpan.FromMilliseconds( 500 + Random.Shared.Next( 5, 50 ) );
clock.UtcNow += timeSpan;
var slices = foo.Tick();
Console.WriteLine( "[{0:hh:mm:ss.ffffff}] {1} ({2})", clock.UtcNow, foo.Distance, slices );
}
foo.Stop();
interface ISystemClock
{
DateTimeOffset UtcNow { get; }
}
class RealTimeClock : ISystemClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
class FakeClock : ISystemClock
{
public DateTimeOffset UtcNow { get; set; } = DateTimeOffset.UtcNow;
}
class Foo
{
private DateTimeOffset _lastTick;
public Foo( ISystemClock systemClock, TimeSpan slice )
{
SystemClock = systemClock;
Slice = slice;
}
public bool IsRunning { get; private set; }
public double ActualSpeed { get; set; }
public double Distance { get; private set; }
public ISystemClock SystemClock { get; }
public TimeSpan Slice { get; }
public int Tick()
{
if ( !IsRunning ) return 0;
var now = SystemClock.UtcNow;
var duration = now - _lastTick;
if (duration < Slice) return 0;
var slices = Convert.ToInt32( duration.TotalMilliseconds / Slice.TotalMilliseconds );
var consumedDuration = slices * Slice;
_lastTick = now - ( duration - consumedDuration );
Distance += ActualSpeed / TimeSpan.FromMinutes( 1 ).TotalMilliseconds * slices * Slice.TotalMilliseconds;
return slices;
}
public void Start()
{
if ( IsRunning ) return;
_lastTick = SystemClock.UtcNow;
IsRunning = true;
}
public void Stop()
{
if ( !IsRunning ) return;
IsRunning = false;
}
}
Das ist ja eine lustige Logik in dem Programm. Auf jeden Fall ist die Userliste immer sehr schön aufgeräumt.
Zitat von DanHue
Ich bin langsam am verzweifeln...
Anscheinend noch nicht genug.
Gibt es vielleicht ein Idee wie man das anders lösen könnte?
Wenn du ernsthaft an einer Lösung interessiert bist dann hättest du schon längst (gleich von Anfang an) ein kleines Demo-Projekt gezeigt anstatt größtmöglichst kompliziert verklausuliert den Ablauf beschrieben bzw. immer nur kleinste Fragmente gezeigt, die aber nicht den gesamten Zusammenhang darstellen. Es gibt viele falsche Wege und auf jeden Fall erheblich weniger richtige Wege. Woher sollen wir wissen, wo genau dein Fehler liegt ohne den gesamten (relevanten) Code zu sehen?
Na endlich.
Und wenn du beim nächsten Mal auch direkt Beispieldaten mitlieferst, dann geht das auch erheblich schneller.
Das Rumgeeiere fing vor 11 Tagen an und zog sich wie ein Kaugummi.
Heute endlich Beispieldaten und nach ein paar Stunden alles geklärt.
Also hier haben wir die Werte der Anschnittscheiben im DataGrid
// Anschnittscheibe
TblProgramminfo.Rows.Add(new object[] { "5075777", "7778888", false, true });
TblProgramminfo.Rows.Add(new object[] { "5075999", "8889999", false, true });
Und diese aus der ComboBox
// SAP-Nummer
TblScheibendaten.Rows.Add(new object[] { "8318555", "250x16x76,2", "Schneid-Formend", "Anschnitt / Fuehren"});
TblScheibendaten.Rows.Add(new object[] { "8318556", "250x16x76,2", "Schneid-Formend", "Anschnitt / Fuehren" });
TblScheibendaten.Rows.Add(new object[] { "8318557", "250x16x76,2", "Schneid-Formend", "Anschnitt / Fuehren" });
Und nach welcher Logik soll da jetzt 7778888
mit welchem der Werte 8318555,8318556,8318557
matchen?
Hier mal dein Beispiel umgesetzt
<Window x:Class="WpfApp.DataWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="DataWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Grid>
<DataGrid x:Name="AutomobileDataGrid"
AutoGenerateColumns="True"
AutoGeneratingColumn="AutomobileDataGrid_AutoGeneratingColumn" />
</Grid>
</Window>
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace WpfApp
{
/// <summary>
/// Interaktionslogik für DataWindow.xaml
/// </summary>
public partial class DataWindow : Window
{
private readonly IList<HerstellerLookup> _herstellerList =
[
new HerstellerLookup { Id = 1, Name = "BMW", },
new HerstellerLookup { Id = 2, Name = "MERCEDES", },
new HerstellerLookup { Id = 3, Name = "OPEL", },
new HerstellerLookup { Id = 4, Name = "VW", },
];
private readonly IList<LackierungLookup> _lackierungList =
[
new LackierungLookup { Id = 1, Name = "Blau", Zusatz = "Alkyd", Pigmentierung = "leicht", },
new LackierungLookup { Id = 2, Name = "Grau", Zusatz = "Alkyd", Pigmentierung = "leicht", },
new LackierungLookup { Id = 3, Name = "Schwarz", Zusatz = "Polyacryl", Pigmentierung = "nein", },
new LackierungLookup { Id = 4, Name = "Weiss", Zusatz = "Polyacryl", Pigmentierung = "nein", },
new LackierungLookup { Id = 5, Name = "Silber", Zusatz = "Alkyd", Pigmentierung = "leicht", },
];
private readonly IList<Automobil> _automobilList =
[
new Automobil { Id = 1, HerstellerId = 4, Baujahr = 2015, LackfarbeId = 1, Kilometerstand = 50_000, },
new Automobil { Id = 2, HerstellerId = 1, Baujahr = 2012, LackfarbeId = 2, Kilometerstand = 100_000, },
new Automobil { Id = 3, HerstellerId = 1, Baujahr = 2019, LackfarbeId = 3, Kilometerstand = 30_000, },
new Automobil { Id = 4, HerstellerId = 2, Baujahr = 2018, LackfarbeId = 4, Kilometerstand = 40_000, },
new Automobil { Id = 5, HerstellerId = 3, Baujahr = 2020, LackfarbeId = 5, Kilometerstand = 50_000, },
];
public DataWindow()
{
InitializeComponent();
AutomobileDataGrid.ItemsSource = _automobilList;
}
private void AutomobileDataGrid_AutoGeneratingColumn( object sender, System.Windows.Controls.DataGridAutoGeneratingColumnEventArgs e )
{
switch ( e.Column.SortMemberPath )
{
case nameof( Automobil.HerstellerId ):
e.Column = new DataGridComboBoxColumn
{
Header = e.Column.Header,
ItemsSource = _herstellerList,
DisplayMemberPath = nameof( HerstellerLookup.Name ),
SelectedValuePath = nameof( HerstellerLookup.Id ),
SelectedValueBinding = new Binding( nameof( Automobil.HerstellerId ) ) { UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, },
};
break;
case nameof( Automobil.LackfarbeId ):
e.Column = new DataGridComboBoxColumn
{
Header = e.Column.Header,
ItemsSource = _lackierungList,
DisplayMemberPath = nameof( LackierungLookup.Name ),
SelectedValuePath = nameof( LackierungLookup.Id ),
SelectedValueBinding = new Binding( nameof( Automobil.LackfarbeId ) ) { UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, },
};
break;
default:
break;
}
}
}
public class Automobil
{
public int? Id { get; set; }
public int? HerstellerId { get; set; }
public int? Baujahr { get; set; }
public int? LackfarbeId { get; set; }
public int? Kilometerstand { get; set; }
}
public class HerstellerLookup
{
public int Id { get; init; }
public string Name { get; init; } = string.Empty;
}
public class LackierungLookup
{
public int Id { get; init; }
public string Name { get; init; } = string.Empty;
public string Zusatz { get; init; } = string.Empty;
public string Pigmentierung { get; init; } = string.Empty;
}
}
In der Spalte "Anschnittscheibe"
steht der Wert "7778888"
und du beklagst dich, dass in der ComboBox
nichts angezeigt wird, wenn in der ComboBox.ItemsSource
folgende Werte enthalten sind "0815", "0816", "4711"
- ist das richtig so?
Dann pack doch mal diese Werte in die Liste für ComboBox.ItemsSource
"0815", "0816", "4711", "7778888"
.
Ist der Wert nicht in der Liste, dann zeigt die ComboBox
nichts an (sieht so aus wie in deinem Bild DataGridComboBoxColumn_04.jpg).
So sollte es passen
var dgComboBoxColAnschnittscheibe = new DataGridComboBoxColumn()
{
Header = "Anschnittscheibe",
Width = new DataGridLength(150),
ItemsSource = new List<string>{ "0815", "0816", "4711" },
SelectedValueBinding = new Binding("Anschnittscheibe") { UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, },
DisplayIndex = 2
};
e.Column = dgComboBoxColAnschnittscheibe;
Zitat von Th69
Zur Info für oehrle: auch bei MVVM in einem ViewModel funktioniert dies (nur eben per Data Binding, anstatt direkt im Code
ItemsSource
zu setzen).
Genau das funktioniert bei mir nicht. Das ging nur im Code. Setzt man die ItemsSource per DataBinding im XAML dann bleibt die ComboBox leer und Snoop-WPF sagt: „Was für eine ItemsSource?“
Zitat von Th69
Ich verstehe nicht, was du als Ausschlußkriterium siehst.
Du hast tatsächlich Recht. Im Code-Behind im Event gesetzt funktioniert das wie gewünscht.
Zitat von Th69
Ich bin mir sicher, daß es auch mit der
DataGridComboBoxColumn
funktioniert, es kommt nur auf die passenden Datentypen an.
Diese Annahme wird weder durch die Dokumentation noch Snoop-Wpf gestützt.
Also im Code-Behind der View kommt eigentlich nur Code rein der zur Visualisierung benötigt wird. Der gesamte Rest nimmt Platz im ViewModel, DataServices, Services usw. In der View muss dir ja auch klar sein, von wo du die Daten beziehst und da das abhängig von den Daten ist, ist das auch im ViewModel bekannt.
Bei dem DataTemplate
und DataEditTemplate
beides Male die ComboBox
eintragen, allerdings im DataTemplate
mit IsEnabled="False"
.
Zitat von oehrle
Ne, der definierte Wert von Anschnittscheibe wird nicht angezeigt, bleibt leer. Eigentlich müßte da der Wert 7778888 angzeigt werden.
Wenn da der Wert 7778888
angezeigt werden soll, dann verstehe ich deine Zeichnung mit dem gelben und dem grünen Pfeil nicht.
"SAP-Nummer" soll als Auswahlkriterium in DataGridComboBox auswählbar sein.
Zu sehen sind die Nummern
8318555
,8318556
,8318557
Ich hätte jetzt vermutet, dass da diese SAP-Nummern stehen sollten.
Egal, so geht das auf jeden Fall nicht, denn das kann die DataGridComboBoxColumn
laut Dokumentation nicht.
Um die Dropdownliste aufzufüllen, legen Sie zunächst die ItemsSource -Eigenschaft für die ComboBox fest, indem Sie eine der folgenden Optionen verwenden:
- Eine statische Ressource. Weitere Informationen finden Sie unter StaticResource-Markuperweiterung.
- Eine x:Static-Codeentität. Weitere Informationen finden Sie unter x:Static Markup Extension.
- Eine Inlineauflistung von ComboBoxItem Typen.
Somit bleibt nur noch die DataGridTemplateColumn
übrig um das zu realisieren.
Zitat von GeneVorph
[…] warum da kein Brush drinne ist. […]
Wenn du Colors
bemühst und die Eigenschaften vom Typ Color
sind (macht ja auch Sinn) wieso erwartest du dann den Typ Brush
?
Wenn du das haben möchtest dann nimm statt Colors
die Klasse Brushes
, denn die hat (wie der Name schon verkündet) Eigenschaften vom Typ Brush
(also den du auch haben möchtest).
Wenn man in die Apfelkiste greift und sich wundert dass da keine Bananen drin sind. 🤔
Du weißt aber schon, dass die statischen Eigenschaften der statischenColors
Klasse vom Typ Color
sind?
Du weißt aber schon, dass die statischen Eigenschaften der statischenBrushes
Klasse vom Typ Brush
(bzw. konkret SolidColorBrush
) sind?
Das casten in eine statische Klasse (z.B. Colors
, Brushes
) ist idR. immer falsch.
Ja, das Timing spielt halt auch eine große Rolle. Darum solltest du auch mit dem Debugger arbeiten und hier z.B. prüfen, welcher Wert in DataContext
steckt. Dann wäre dir auch aufgefallen, dass der zu dem Zeitpunkt ebenfalls null
ist.
Wenn du mal beschreiben könntest was du wirklich vorhast - also mehr Details als "ich will Daten mitnehmen" - dann könnte man dir wirklich Beispiel-Code geben.
Nun ja du kannst casten
var viewmodel = DataContext as MyClass;
sowie weitere viele Dinge die aber alles Grundlagen von C# sind
wenn ich MyClass in Window1 in Codebehind lade, das dieser dann leer ist.
Hmmm, es ist auch hilfreich die richtige Terminologie zu verwenden. Wenn du eine neue Instanz erzeugst, dann wird diese eben nicht geladen, sondern erzeugt und ist auch nicht leer sondern enthält die default Werte.
Problematisch ist, das man dann oft aneinander vorbei redet weil man es falsch beschreibt/benennt.
Lass es mich so erklären:
Du hast einen Korb und legst dort einen Apfel hinein. Wenn dein Bruder jetzt in diesen Korb hineinschaut, was sieht er? Genau, diesen einen Apfel.
Jetzt nimmt dein Bruder einen neuen Korb, der deinem Korb erstaunlich ähnlich ist, und schaut in diesen, was sieht er? Genau, gähnende Leere.
Wenn du erklären kannst, warum das so ist, dann hast du dein Problem erklärt.
Dir ist schon bewusst, dass wir keine Möglichkeit haben auf deinen Bildschirm oder deine Festplatte zu schauen?
Warum zeigst du uns nicht eine abgespeckte aber in sich lauffähige Version die (nur) dein Problem zeigt? Es ist sehr ermüdend jeden Popel einzeln aus der Nase heraus zu ziehen nur um festzustellen, dass da noch mehr ist was relevant aber für uns unbekannt ist.
In solch einer abgespeckten Version entfernt man z.B. dieses ganze Dialog und Datei Geraffel und liefert einfach eine statisches oder generiertes Ergebnis zurück.
Du hast da eine Liste UnderlyingList
die du leerst und wieder füllst, die aber in der View nicht verwendet wird (oder willst du das nicht zeigen?).
Du hast zwei Eigenschaften Underlying1
und Underlying2
wo es in der View eine Bindung mit der ersten gibt, diese werden aber in der ButtonLoadExecute
Methode nicht aktualisiert.
Und nun erwartest du das die View darauf irgendwie reagiert. Warum?
UnderlyingList
deklariert ist?ObservableCollection
obwohl du diese niemals als Observable einsetzt? Ein Array hätte hier gereicht.Underlying
Klasse wäre auch hilfreich, fehlt aber.Underlying1
- was auch immer das sein soll und wie auch immer das gefüllt wird.Und nur am Rand bemerkt:
Bei der Load
Methode vermengst du UI mit Infrastruktur und Präsentation. Besser wäre im ViewModel ein LoadCommand der als CommandParameter die gewählte Datei übergeben bekommt. In der View rufst du den Dialog auf und wenn das erfolgreich war, dann führst du den LoadCommand aus. Und ja, das kommt in das Code-Behind der View und ist nicht schlimm.
Im Anhang habe ich dir ein komplettes WPF-Projekt angehängt.
Allerdings habe ich den Datenzugriff mit einem Fake-DataService vereinfacht. Du kannst dir aber einen eigenen DataService schreiben, der dann auf die Datenbank zugreift.
Ist das AV.RP_NEED, PROD.RP_NEED, MONT.RP_NEED
(C# Code) wirklich gleich AV.RP_NEED as AV, PROD.RP_NEED as PROD, MONT.RP_NEED as MONT
(SQL-Statement im Management Studio) und wenn nicht, was könnte das für Auswirkungen haben?
Vorab: Es gibt hier Code-Blocks die kann man verwenden, damit der Code zu 3245% lesbarer ist als ohne.
Eine ComboBox
ist abgeleitet von einem ItemsControl
und dieses hat folgende Eigenschaften die relevant sind:
DisplayMemberPath
SelectedValuePath
SelectedValue
Wenn du im ViewModel
eine Liste hast mit den Anreden,
public ICollection<Anreden> Anreden { get∞ }
[Reactive] public int? SelectedAnredeId { get; set; } // hier steht der gewählte Wert
dann kannst du diese wie folgt an die ComboBox
binden
<ComboBox ItemsSource="{Binding Anreden}"
SelectedValue="SelectedAnredeId"
SelectedValuePath="Anrede_id"
DisplayMemberPath="Anrede"/>
PS: Hast du bemerkt wie gut man den Code in diesem Beitrag sehen kann? Das machen diese Code-Blocks
Versuch es doch mal mit
<Image Source="/TextEditor;component/Images/bold.png"/>
Also mit einem Slash vor dem Bibliotheks-Namen
Aus diesen Daten
create table RP_PROJECT
(
ID int not null,
primary key (ID)
);
create table RP_PROJECT_DATA
(
RP_PROJECT_ID int not null,
RP_REIHENFOLGE int not null,
RP_OPERATION varchar(4) not null,
RP_NEED float not null
);
insert into RP_PROJECT (ID)
VALUES
(251),
(253),
(254);
insert into RP_PROJECT_DATA (RP_PROJECT_ID, RP_REIHENFOLGE, RP_OPERATION, RP_NEED)
VALUES
(251, 1, 'AV', 170.61),
(251, 2, 'PROD', 457.535619),
(251, 3, 'MONT', 147.341979),
(253, 1, 'AV', 52.8),
(253, 2, 'PROD', 139.2),
(253, 3, 'MONT', 48),
(254, 1, 'AV', 38.929);
bekommst du so deine Werte in einer Zeile
select ID RP_PROJECT_ID, AV.RP_NEED AV, PROD.RP_NEED PROD, MONT.RP_NEED MONT
from RP_PROJECT
left join RP_PROJECT_DATA AV on AV.RP_PROJECT_ID=ID and AV.RP_OPERATION='AV'
left join RP_PROJECT_DATA PROD on PROD.RP_PROJECT_ID=ID and PROD.RP_OPERATION='PROD'
left join RP_PROJECT_DATA MONT on MONT.RP_PROJECT_ID=ID and MONT.RP_OPERATION='MONT'
Live-Beispiel SQL Fiddle
Mein Beispiel-Projekt setzt voll auf DI und jedes Window
wird mittels einem DI-Container erzeugt und der sorgt dafür, dass eben diese Abhängigkeiten (Dependencies) im Konstruktor automatisch übergeben (injected) werden.
Verantwortlich dafür ist Wpf.Extensions.Hosting . Verkabelt wird das in der Program.cs
// Create a builder by specifying the application and main window.
using WpfApp;
var builder = WpfApplication<App, MainWindow>.CreateBuilder( args );
// Configure dependency injection.
// Injecting MainWindowViewModel into MainWindow.
builder.Services
.AddTransient<MainWindowViewModel>()
.AddTransient<PersonViewModel>()
.AddTransient<AnimalViewModel>()
.AddTransient<CombinedViewModel>()
;
var app = builder.Build();
await app.RunAsync();
In WPF kann man für einen Typ ein DataTemplate
festlegen und genau das habe ich in der App.xaml
gemacht, denn dort angelegt gilt dass dann für die gesamte Anwendung.
<DataTemplate DataType="{x:Type viewmodels:CombinedViewModel}">
<views:CombinedControl />
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:AnimalViewModel}">
<StackPanel>
<DockPanel>
<Label Content="Species:"
DockPanel.Dock="Top" />
<TextBox Text="{Binding Species, UpdateSourceTrigger=PropertyChanged}" />
</DockPanel>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:PersonViewModel}">
<StackPanel>
<DockPanel>
<Label Content="Name:"
DockPanel.Dock="Top" />
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
</DockPanel>
</StackPanel>
</DataTemplate>
Ja, in der Beispiel-Anwendung steckt eine ganze Menge an (praktischen) Features. Mir ist es aber auch wichtig zu zeigen, wie diese Features zusammenarbeiten - auch wenn es am Anfang einen erschlagen könnte.
Wenn Person und Animal im Combined als Eigenschaft vorhanden sind, dann braucht man die eigentlich nicht nochmal im MainViewModel. Und wenn du mit einem DI-Container arbeitest, dann brauchst du auch keine Instanzen erzeugen, sondern übergibst die automatisch.
namespace Fictiv.ViewModel
{
public class MainViewModel
{
public ViewModelCombine Combine { get; }
public MainViewModel( ViewModelCombine combine ) // per DI übergeben
{
Combine = combine;
}
}
public class ViewModelCombine : ObservableObject
{
// the viewmodel have to know the model.
private readonly Model.Combine CombineModel;
public ViewModelPerson Person { get; }
public ViewModelAnimal Animal { get; }
private string combineCombinedText;
public string Bind_combineCombinedText
{
get { return combineCombinedText; }
set { SetProperty(ref combineCombinedText, value); }
}
// button to combine person with animal text
public ICommand Bind_btnCombineCommand { get; }
// Constructor
public ViewModelCombine(ViewModelPerson person, ViewModelAnimal animal)
{
Bind_combineCombinedText = "EMPTY";
// handle binding methods
Bind_btnCombineCommand = new RelayCommand(BtnCombineCommand);
Person = person;
Animal = animal;
}
private void BtnCombineCommand()
{
//Bind_combineCombinedText = "Button has been pressed";
Bind_combineCombinedText = Person.Bind_personNameText;
Bind_combineCombinedText += " has a ";
Bind_combineCombinedText += Animal.Bind_animalSpeciesText;
}
}
}
BTW: Falls du es mal mit ReactiveUI versuchen möchtest, da sieht so etwas ein wenig cleaner aus
public class MainWindowViewModel : ViewModelBase
{
public CombinedViewModel Combined { get; }
public MainWindowViewModel( CombinedViewModel combined )
{
Combined = combined;
}
}
public class CombinedViewModel : ViewModelBase
{
public AnimalViewModel Animal { get; }
public PersonViewModel Person { get; }
public ReactiveCommand<Unit, Unit> CombineCommand { get; }
[Reactive] public string? Combined { get; private set; }
public CombinedViewModel( AnimalViewModel animal, PersonViewModel person )
{
Animal = animal;
Person = person;
var canCombine = this.WhenAnyValue( e => e.Person.Name, e => e.Animal.Species, ( name, species ) => !string.IsNullOrEmpty( name ) && !string.IsNullOrEmpty( species ) );
CombineCommand = ReactiveCommand.CreateFromTask( OnCombine, canCombine );
}
private async Task OnCombine( CancellationToken cancellationToken )
{
await Task.Delay( 250 );
Combined = $"{Person.Name} - {Animal.Species}";
}
}
public class PersonViewModel : ViewModelBase
{
[Reactive] public string? Name { get; set; }
}
public class AnimalViewModel : ViewModelBase
{
[Reactive] public string? Species { get; set; }
}
Und im Anhang das in einem Beispiel-Projekt mit DI-Container usw.
ViewModel können Daten auch über einen Messaging Dienst austauschen (z.B. ReactiveUI.MessageBus
) allerdings würde ich in deinem Beispiel eher dahin tendieren dem MainWindowViewModel
alle benötigten ViewModel per DI zu übergeben und als Eigenschaft zur Verfügung zu stellen.
Eigentlich legt doch der Frame - in dem diese Page angezeigt wird - fest, wie groß selbige dargestellt wird. So ist wenigstens mein Kenntnisstand.
Bei mir gilt der Grundsatz: Make it work (smart and readable), then fast (if needed). Und es ist schnell genug für diesen Anwendungsfall.
Meine Lösung sieht so aus:
public int GetAnswerTwo()
{
return GetInput() // puzzle input as IEnumerable<string>
.Select( e => new string( [GetFirstDigit( e ), GetLastDigit( e )] ) )
.Select( int.Parse )
.Sum();
}
private static readonly Dictionary<string, char> _digits = new Dictionary<string, char>
{
{ "1", '1' },
{ "2", '2' },
{ "3", '3' },
{ "4", '4' },
{ "5", '5' },
{ "6", '6' },
{ "7", '7' },
{ "8", '8' },
{ "9", '9' },
{ "one", '1' },
{ "two", '2' },
{ "three", '3' },
{ "four", '4' },
{ "five", '5' },
{ "six", '6' },
{ "seven", '7' },
{ "eight", '8' },
{ "nine", '9' },
};
public static char GetFirstDigit( string s ) =>
_digits
.Select( e => (Value: e.Value, Index: s.IndexOf( e.Key )) )
.Where( e => e.Index >= 0 )
.OrderBy( e => e.Index )
.Select( e => e.Value )
.FirstOrDefault( '0' );
public static char GetLastDigit( string s ) =>
_digits
.Select( e => (Value: e.Value, Index: s.LastIndexOf( e.Key )) )
.Where( e => e.Index >= 0 )
.OrderByDescending( e => e.Index )
.Select( e => e.Value )
.FirstOrDefault( '0' );
Nun stell dir vor, du hast folgenden Eingangswert: oneight
dann liefert deine Variante als Zahl 11
zurück. Richtig wäre allerdings 18
.
Und noch etwas zum Lesen
Zur Information:
Zitat von CSharpNewbie2022
ich habe eine Liste von Informationen und ich möchte diese grafische darstellen. [...] Ich möchte das mit dem MVVM Design Pattern lösen.
Du benötigst ein Control, was dir eine Liste mit Strings so wie vorgegeben darstellt. Der von dir erwähnte MVVM Teil beschränkt sich dabei darauf, dass du im ViewModel solch eine Liste mit Strings zur Verfügung stellst und diese dann an das Control bindest. Fertig.
Du kannst also das MVVM Gedöns erst einmal vernachlässigen und dich auf das Design des Controls stürzen.
Wo ist denn das Problem bzw. wo ist das von dir erwartete Verhalten dokumentiert.
Wenn die Validierung fehl schlägt, dann bleibt bei entsprechender Einstellung der Focus auf der TextBox
. Somit können andere Elemente, die den Focus benötigen nicht mehr erreicht werden. EinLabel
gehört aber nicht zu diesen "Ich brauche den Focus" Elementen.