Laden...

Forenbeiträge von Pseudonym4711 Ingesamt 55 Beiträge

31.01.2018 - 17:36 Uhr

Hallo Rabenschwinge,

falls ich dich richtig verstanden habe, so geht das sogar ziemlich einfach


<UserControl x:Class="MVVMTestProjekt.Views.TestView2"
             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:vm="clr-namespace:MVVMTestProjekt.ViewModels"
             mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"
             d:DataContext="{d:DesignInstance {x:Type vm:TestViewModel}}">
    <Grid>
            
    </Grid>
    
</UserControl>


<UserControl x:Class="MVVMTestProjekt.Views.TestView3"
             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:vm="clr-namespace:MVVMTestProjekt.ViewModels"
             mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"
             d:DataContext="{d:DesignInstance {x:Type vm:TestViewModel}}">
    <Grid>
            
    </Grid>
</UserControl>

31.01.2018 - 17:19 Uhr

Hallo Zusammen,

Ich habe den Code jetzt noch mal komplett überarbeitet, damit vielleicht eine etwas verständlichere Variante daraus geworden ist.

Es gibt 3 Objekte in meiner Testanwendung (Beispielhaft siehe Code).


public class Vorname : ViewModelBase
{
    int _vornameId;
    public int VornameId { get { return _vornameId; } set { _vornameId = value; OnPropertyChanged(); } }

    string _name;
    public string Name { get { return _name; } set { _name = value; OnPropertyChanged(); } }
}


public class Nachname : ViewModelBase
{
    int _nachnameId;
    public int NachnameId { get { return _nachnameId; } set { _nachnameId = value; OnPropertyChanged(); } }

    int _vorname_vornameId;
    public int Vorname_VornameId { get { return _vorname_vornameId; } set { _vorname_vornameId = value; OnPropertyChanged(); } }

    string _name;
    public string Name { get { return _name; } set { _name = value; OnPropertyChanged(); } }
}


public class Namensbewertung : ViewModelBase
{
    int _namensbewertungId;
    public int NamensbewertungId { get { return _namensbewertungId; } set { _namensbewertungId = value; OnPropertyChanged(); } }

    int _vorname_VornameId;
    public int Vorname_VornameId { get { return _vorname_VornameId; } set { _vorname_VornameId = value; OnPropertyChanged(); } }

    int _nachname_NachnameId;
    public int Nachname_NachnameId { get { return _nachname_NachnameId; } set { _nachname_NachnameId = value; OnPropertyChanged(); } }

    string _bewertung;
    public string Bewertung { get { return _bewertung; } set { _bewertung = value; OnPropertyChanged(); } }
}

Im ViewModel erstell ich ein paar Testdaten


public TestViewModel()
{
    base.DisplayName = Strings.MainWindowViewModel_Command_ViewTest;

    Vornamen = new ObservableCollection<Vorname>(
        new List<Vorname>()
            {
                new Vorname { VornameId = 1, Name = "Hans" },
                new Vorname { VornameId = 2, Name = "Klaus" },
                new Vorname { VornameId = 3, Name = "Berta" }
            });

    Nachnamen = new ObservableCollection<Nachname>(
        new List<Nachname>()
            {
                new Nachname { NachnameId = 1, Vorname_VornameId = 1, Name = "Müller" },
                new Nachname { NachnameId = 2, Vorname_VornameId = 1, Name = "Schmidt" },
                new Nachname { NachnameId = 3, Vorname_VornameId = 1, Name = "Schulze" },
                new Nachname { NachnameId = 4, Vorname_VornameId = 2, Name = "Eder" },
                new Nachname { NachnameId = 5, Vorname_VornameId = 2, Name = "Meier" },
                new Nachname { NachnameId = 6, Vorname_VornameId = 2, Name = "Merkel" },
                new Nachname { NachnameId = 7, Vorname_VornameId = 3, Name = "Schröder" },
                new Nachname { NachnameId = 8, Vorname_VornameId = 3, Name = "Gates" },
                new Nachname { NachnameId = 9, Vorname_VornameId = 3, Name = "Schneider" }
            });

    Namensbewertungen = new ObservableCollection<Namensbewertung>(
        new List<Namensbewertung>()
            {
                new Namensbewertung { NamensbewertungId = 1, Vorname_VornameId = 1, Nachname_NachnameId = 1, Bewertung = "Schöner Name"},
                new Namensbewertung { NamensbewertungId = 2, Vorname_VornameId = 1, Nachname_NachnameId = 3, Bewertung = "Wahnsinns Name"},
                new Namensbewertung { NamensbewertungId = 3, Vorname_VornameId = 2, Nachname_NachnameId = 4, Bewertung = "Was für ein Name"},
                new Namensbewertung { NamensbewertungId = 4, Vorname_VornameId = 2, Nachname_NachnameId = 5, Bewertung = "WTF!!!"},
                new Namensbewertung { NamensbewertungId = 5, Vorname_VornameId = 3, Nachname_NachnameId = 8, Bewertung = "Ist das ein Name"},
                new Namensbewertung { NamensbewertungId = 6, Vorname_VornameId = 3, Nachname_NachnameId = 9, Bewertung = "Wer heißt denn bitte so?"}
            });
}

Das Ganze wird dann folgendermaßen in der View an ein DataGird angebunden.


<UserControl x:Class="MVVMTestProjekt.Views.TestView"
             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:vm="clr-namespace:MVVMTestProjekt.ViewModels"
             mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="600"
             xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
             d:DataContext="{d:DesignInstance {x:Type vm:TestViewModel}}" FontSize="16">
    
    <Grid>

        <DataGrid AutoGenerateColumns="False"
                  CanUserAddRows="False"
                  ItemsSource="{Binding Namensbewertungen, UpdateSourceTrigger=PropertyChanged}">

            <DataGrid.Columns>

                <DataGridTextColumn Header="NamensbewertungId"
                                    Binding="{Binding NamensbewertungId, UpdateSourceTrigger=PropertyChanged}" />

                <DataGridComboBoxColumn Header="Vorname_VornameId"
                                        SelectedValueBinding="{Binding Vorname_VornameId, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
                                        SelectedValuePath="VornameId"
                                        DisplayMemberPath="Name">

                    <DataGridComboBoxColumn.ElementStyle>
                        <Style TargetType="ComboBox"
                               BasedOn="{StaticResource {x:Type ComboBox}}">
                            <Setter Property="ItemsSource"
                                    Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, 
                                                Path=DataContext.Vornamen, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
                        </Style>
                    </DataGridComboBoxColumn.ElementStyle>

                </DataGridComboBoxColumn>

                <DataGridComboBoxColumn Header="Nachname_NachnameId"
                                        SelectedValueBinding="{Binding Nachname_NachnameId, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
                                        SelectedValuePath="NachnameId"
                                        DisplayMemberPath="Name">
                    <DataGridComboBoxColumn.ElementStyle>
                        <Style TargetType="ComboBox"
                               BasedOn="{StaticResource {x:Type ComboBox}}">
                            
                            <Setter Property="ItemsSource"
                                    Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}},
                                                Path=DataContext.Nachnamen, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
                            
                        </Style>
                        
                    </DataGridComboBoxColumn.ElementStyle>

                </DataGridComboBoxColumn>

                <DataGridTextColumn Header="Bewertung"
                                    Binding="{Binding Bewertung, UpdateSourceTrigger=PropertyChanged}" />

            </DataGrid.Columns>
                
        </DataGrid>
        
    </Grid>
</UserControl>

Die ItemsSource der ersten CoboBox sind die Vornamen.
Die ItemsSource der zweiten Combobox sind die Nachnamen.
Die SelectedItems entsprechen jeweils der in Namensbewertungen gespeicherten "FremdId's" (siehe erstes Bild).

Wenn ich nun die zweite ComboBox aufklappe werden alle Nachnamen aufgelistet (siehe zweites Bild).

Ich möchte aber, dass in der zweiten CombBox nur die Nachnamen angezeigt werden, die zum jeweiligen Vornamen - in der ComboBox davor - gemäß ihrer ID's gehören. So eine Art Filter also (siehe bearbeitetes drittes Bild).

Könntet ihr mir hier Tipps geben, wie ich das realisieren könnte?

Danke und Gruß
Torsten

15.01.2018 - 11:58 Uhr

Danke Jungs,

das beantwortet alles, ich hatte mich falsch herum ausgedrückt. 😉

12.01.2018 - 16:31 Uhr

Vielen Dank euch beiden für eure Antworten!

@Abt

Verstehe ich dich richtig, dass wenn hier nichts weiteres gesteuert/geschaltet wird, wie zum Beispiel hier:

public class MyClass : NotifyPropertyChanged
{
    string _firstName;
    public string FirstName { get => _firstName; set { _firstName = value; OnPropertyChanged; } }
} 

Es noch einfacher gehalten werden sollte so wie hier?

public class MyClass
{
    public string FirstName { get; set; }
} 
08.01.2018 - 20:46 Uhr

Hallo Abt,

besten Dank für deine Antwort.

Es ist eine Visual Studio Community 2017 Standardinstallation ohne irgendein Plugin.
Ich weiß dass man in den Optionen einiges ändern kann, aber davon
lass ich eigentlich auch immer die Finger.

08.01.2018 - 20:28 Uhr

Hallo Zusammen,

ich habe nach längerer Pause mal wieder meine Entwicklungsumgebung geöffnet
und festgestellt, dass mir mein VS17 anstelle von Eigenschaften auf einmal Methoden vorschlägt.

Bisher schrieb ich es immer so:

public class MyClass
{
    string _firstName;
    public string FirstName { get => _firstName; set => _firstName = value; }
}

Der Vorschlag vom VS lautet neuerdings aber:

public class MyClass
{
    string _firstName;
    public string GetFirstName() => _firstName;
    public void SetFirstName(string value) { _firstName = value; }
}

Ist das jetzt die neue "Empfehlung", dass man das jetzt so machen sollte?

Viele Grüße
Torsten

26.07.2017 - 13:20 Uhr

Besten Dank für den Link! Für meinen Zweck zwar ganz schön mächtig,
aber der Weg ist dann legal. 😃

26.07.2017 - 12:11 Uhr

dank a hat sich b ja erübrigt . . .

Danke dir trotzdem

26.07.2017 - 11:12 Uhr

Hallo zusammen,

ich möchte für meine Familie privat gerade eine art Haushaltsbuch programmieren
und möchte, dass mein Programm auch die aktuellen Kontostände und Umsätze
holt.

Bei meinem Konto (Postbank) funktioniert das alles recht einfach (Login, Seitennavigation,
Inhalte auslesen und wieder ausloggen).

Beim Konto meiner Frau (Commerzbank) ist das schon ne ganze Spur aufwändiger.

Hier muss die Loginseite erst mal per GET aufgerufen werden, um an den sogenannten
pToken heranzukommen den ich danach im POST brauche.

Damit klappt zwar der Login wunderbar, aber der Kontostand und die Umsätze werden
auf der Seite verzögert nachgeladen und ich weiß nicht wie ich auf diesen verzögerten
Inhalt warten kann.

Ich habe es schon async probiert (siehe auch folgender Code), aber die Seite an sich
ist schnell geladen und meldet dann wahrscheinlich "ich bin fertig", obwohl diverse
Inhalte eben noch fehlen.


using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

public class Commerzbank
{
    CookieContainer cookieContainer = new CookieContainer();

    HttpWebRequest request;
    HttpWebResponse response;

    byte[] byteArray;

    string postData, sourceCode, pToken;

    public Commerzbank()
    {
        request = (HttpWebRequest)WebRequest.Create("https://kunden.commerzbank.de/lp/login/?-1.IFormSubmitListener-loginForm");
        request.CookieContainer = cookieContainer;
        request.Accept = "text/html, application/xhtml+xml, image/jxr, */*";
        request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko";
        request.ContentType = "application/x-www-form-urlencoded";
        request.Method = "GET";

        sourceCode = ReadStreamFromResponse((HttpWebResponse)request.GetResponse());

        File.WriteAllText(@"C:\Users\User\Desktop\Commerzbank1.html", sourceCode);

        Regex getPToken = new Regex(@"(?<=pToken:..)(.*?,)");
        pToken = getPToken.Match(sourceCode).ToString().Substring(0, (getpToken.Match(sourceCode).ToString().Length - 2));

        var responseTask = GetResponseAsync();
        sourceCode = responseTask.Result;

        File.WriteAllText(@"C:\Users\User\Desktop\Commerzbank2.html", sourceCode);

        Task<string> GetResponseAsync()
        {
            request = (HttpWebRequest)WebRequest.Create("https://kunden.commerzbank.de/lp/login/?-1.IFormSubmitListener-loginForm");
            request.CookieContainer = cookieContainer;
            request.Accept = "text/html, application/xhtml+xml, image/jxr, */*";
            request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko";
            request.ContentType = "application/x-www-form-urlencoded";
            request.Method = "POST";
            request.Timeout = 20000;
            request.Proxy = null;

            postData = "pToken=" + pToken;
            postData += ("id1_hf_0=" + "");
            postData += ("&userid=" + "Benutzername/Teilnehmernummer");
            postData += ("&pin=" + "PIN");
            postData += ("&startSite=" + "/banking/landingpage");

            byteArray = Encoding.UTF8.GetBytes(postData);
            request.ContentLength = byteArray.Length;

            using (Stream stream = request.GetRequestStream())
            {
                stream.Write(byteArray, 0, byteArray.Length);
            }

            Task<WebResponse> task = Task.Factory.FromAsync(
                request.BeginGetResponse, asyncResult => request.EndGetResponse(asyncResult), null);

            return task.ContinueWith(t => ReadStreamFromResponse(t.Result));

        }

        string ReadStreamFromResponse(WebResponse response)
        {
            using (Stream responseStream = response.GetResponseStream())
            using (StreamReader sr = new StreamReader(responseStream))
            {
                string strContent = sr.ReadToEnd();
                return strContent;
            }
        }
    }
}

Das viele Grün im Code oben sind übrigens keine Kommentare, sondern die Seite hier interpretiert das nur so.

So und jetzt noch ein screenshot aus "Commerzbank2.html" (als Dateianhang). Da sieht man schön diese "ich lade noch Platzhalter".

Wie kann ich jetzt diesen Ladevorgang abwarten?

Vielen Dank schon mal für eure Hilfe!
Viele Grüße
Torsten

12.01.2017 - 13:21 Uhr

besten Dank für eure Antworten!

Wieder was dazu gelernt.

12.01.2017 - 13:15 Uhr

Hallo Zusammen,

ich dachte bisher, dass die folgende Schreibweise für eine Eigenschaft eigentlich gängig ist und den Benennungsrichtlinien entspricht.


string name { get; set; }
public string Name { get => name; set { name = value; OnPropertyChanged(); } }

Jetzt meckert mich das VS2017 aber mit der angehängten Meldung an:

Klar kann man sich jetzt eigene Benennungsregeln hinzufügen, oder die Warnungen vielleicht unterdrücken, aber mich würde interessieren, wie die
offizielle empfohlene Schreibweise von Microsoft aussieht, damit diese Meldung nicht mehr kommt. Hat da einer von euch eine Ahnung?
Wenn ich den private jetzt groß schreibe, wie benenne ich denn dann den public?

Danke und Gruß
Torsten

08.05.2015 - 12:54 Uhr

Hallo Zusammen,

Ich habe ein kleines Programm das PDF Dateien per DragDrop auslesen kann und das funktioniert wunderbar.

Diese PDF Dateien kommen immer per E-Mail und es wäre schön, wenn man da den Anhang nicht immer zwischenspeichern müsste.

Ich habe im Netz ein DragDrop Beispiel in Windows Forms gefunden und
inzwischen auch die Übersetzung in WPF, aber alles was das Programm unter dem Punkt "Files" liefert ist das Outlookobjekt selbst.

Ich möchte aber den Anhang innerhalb des Outlookobjektes haben.

Hat hier jemand vielleicht eine Idee?

Vielen Dank schon mal!

Hier der aktuelle verwendete Code:


using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Reflection;
using System.Windows;

namespace OutlookDragDropTestWPF
{
    /// <summary>
    /// Provides a format-independant machanism for transfering data with support for outlook messages and attachments.
    /// </summary>
    public class OutlookDataObject : System.Windows.IDataObject
    {
        #region NativeMethods

        private class NativeMethods
        {
            [DllImport("kernel32.dll")]
            static extern IntPtr GlobalLock(IntPtr hMem);

            [DllImport("ole32.dll", PreserveSig = false)]
            public static extern ILockBytes CreateILockBytesOnHGlobal(IntPtr hGlobal, bool fDeleteOnRelease);

            [DllImport("OLE32.DLL", CharSet = CharSet.Auto, PreserveSig = false)]
            public static extern IntPtr GetHGlobalFromILockBytes(ILockBytes pLockBytes);

            [DllImport("OLE32.DLL", CharSet = CharSet.Unicode, PreserveSig = false)]
            public static extern IStorage StgCreateDocfileOnILockBytes(ILockBytes plkbyt, uint grfMode, uint reserved);

            [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("0000000B-0000-0000-C000-000000000046")]
            public interface IStorage
            {
                [return: MarshalAs(UnmanagedType.Interface)]
                IStream CreateStream([In, MarshalAs(UnmanagedType.BStr)] string pwcsName, [In, MarshalAs(UnmanagedType.U4)] int grfMode, [In, MarshalAs(UnmanagedType.U4)] int reserved1, [In, MarshalAs(UnmanagedType.U4)] int reserved2);
                [return: MarshalAs(UnmanagedType.Interface)]
                IStream OpenStream([In, MarshalAs(UnmanagedType.BStr)] string pwcsName, IntPtr reserved1, [In, MarshalAs(UnmanagedType.U4)] int grfMode, [In, MarshalAs(UnmanagedType.U4)] int reserved2);
                [return: MarshalAs(UnmanagedType.Interface)]
                IStorage CreateStorage([In, MarshalAs(UnmanagedType.BStr)] string pwcsName, [In, MarshalAs(UnmanagedType.U4)] int grfMode, [In, MarshalAs(UnmanagedType.U4)] int reserved1, [In, MarshalAs(UnmanagedType.U4)] int reserved2);
                [return: MarshalAs(UnmanagedType.Interface)]
                IStorage OpenStorage([In, MarshalAs(UnmanagedType.BStr)] string pwcsName, IntPtr pstgPriority, [In, MarshalAs(UnmanagedType.U4)] int grfMode, IntPtr snbExclude, [In, MarshalAs(UnmanagedType.U4)] int reserved);
                void CopyTo(int ciidExclude, [In, MarshalAs(UnmanagedType.LPArray)] Guid[] pIIDExclude, IntPtr snbExclude, [In, MarshalAs(UnmanagedType.Interface)] IStorage stgDest);
                void MoveElementTo([In, MarshalAs(UnmanagedType.BStr)] string pwcsName, [In, MarshalAs(UnmanagedType.Interface)] IStorage stgDest, [In, MarshalAs(UnmanagedType.BStr)] string pwcsNewName, [In, MarshalAs(UnmanagedType.U4)] int grfFlags);
                void Commit(int grfCommitFlags);
                void Revert();
                void EnumElements([In, MarshalAs(UnmanagedType.U4)] int reserved1, IntPtr reserved2, [In, MarshalAs(UnmanagedType.U4)] int reserved3, [MarshalAs(UnmanagedType.Interface)] out object ppVal);
                void DestroyElement([In, MarshalAs(UnmanagedType.BStr)] string pwcsName);
                void RenameElement([In, MarshalAs(UnmanagedType.BStr)] string pwcsOldName, [In, MarshalAs(UnmanagedType.BStr)] string pwcsNewName);
                void SetElementTimes([In, MarshalAs(UnmanagedType.BStr)] string pwcsName, [In] System.Runtime.InteropServices.ComTypes.FILETIME pctime, [In] System.Runtime.InteropServices.ComTypes.FILETIME patime, [In] System.Runtime.InteropServices.ComTypes.FILETIME pmtime);
                void SetClass([In] ref Guid clsid);
                void SetStateBits(int grfStateBits, int grfMask);
                void Stat([Out]out System.Runtime.InteropServices.ComTypes.STATSTG pStatStg, int grfStatFlag);
            }

            [ComImport, Guid("0000000A-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
            public interface ILockBytes
            {
                void ReadAt([In, MarshalAs(UnmanagedType.U8)] long ulOffset, [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] pv, [In, MarshalAs(UnmanagedType.U4)] int cb, [Out, MarshalAs(UnmanagedType.LPArray)] int[] pcbRead);
                void WriteAt([In, MarshalAs(UnmanagedType.U8)] long ulOffset, IntPtr pv, [In, MarshalAs(UnmanagedType.U4)] int cb, [Out, MarshalAs(UnmanagedType.LPArray)] int[] pcbWritten);
                void Flush();
                void SetSize([In, MarshalAs(UnmanagedType.U8)] long cb);
                void LockRegion([In, MarshalAs(UnmanagedType.U8)] long libOffset, [In, MarshalAs(UnmanagedType.U8)] long cb, [In, MarshalAs(UnmanagedType.U4)] int dwLockType);
                void UnlockRegion([In, MarshalAs(UnmanagedType.U8)] long libOffset, [In, MarshalAs(UnmanagedType.U8)] long cb, [In, MarshalAs(UnmanagedType.U4)] int dwLockType);
                void Stat([Out]out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, [In, MarshalAs(UnmanagedType.U4)] int grfStatFlag);
            }

            [StructLayout(LayoutKind.Sequential)]
            public sealed class POINTL
            {
                public int x;
                public int y;
            }

            [StructLayout(LayoutKind.Sequential)]
            public sealed class SIZEL
            {
                public int cx;
                public int cy;
            }

            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
            public sealed class FILEGROUPDESCRIPTORA
            {
                public uint cItems;
                public FILEDESCRIPTORA[] fgd;
            }

            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
            public sealed class FILEDESCRIPTORA
            {
                public uint dwFlags;
                public Guid clsid;
                public SIZEL sizel;
                public POINTL pointl;
                public uint dwFileAttributes;
                public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
                public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
                public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
                public uint nFileSizeHigh;
                public uint nFileSizeLow;
                [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
                public string cFileName;
            }

            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
            public sealed class FILEGROUPDESCRIPTORW
            {
                public uint cItems;
                public FILEDESCRIPTORW[] fgd;
            }

            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
            public sealed class FILEDESCRIPTORW
            {
                public uint dwFlags;
                public Guid clsid;
                public SIZEL sizel;
                public POINTL pointl;
                public uint dwFileAttributes;
                public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
                public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
                public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
                public uint nFileSizeHigh;
                public uint nFileSizeLow;
                [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
                public string cFileName;
            }
        }

        #endregion

        #region Property(s)

        /// <summary>
        /// Holds the <see cref="System.Windows.Forms.IDataObject"/> that this class is wrapping
        /// </summary>
        private System.Windows.IDataObject underlyingDataObject;

        /// <summary>
        /// Holds the <see cref="System.Runtime.InteropServices.ComTypes.IDataObject"/> interface to the <see cref="System.Windows.Forms.IDataObject"/> that this class is wrapping.
        /// </summary>
        private System.Runtime.InteropServices.ComTypes.IDataObject comUnderlyingDataObject;

        /// <summary>
        /// Holds the internal ole <see cref="System.Windows.Forms.IDataObject"/> to the <see cref="System.Windows.Forms.IDataObject"/> that this class is wrapping.
        /// </summary>
        private System.Windows.IDataObject oleUnderlyingDataObject;

        /// <summary>
        /// Holds the <see cref="MethodInfo"/> of the "GetDataFromHGLOBLAL" method of the internal ole <see cref="System.Windows.Forms.IDataObject"/>.
        /// </summary>
        private MethodInfo getDataFromHGLOBLALMethod;

        #endregion

        #region Constructor(s)

        /// <summary>
        /// Initializes a new instance of the <see cref="OutlookDataObject"/> class.
        /// </summary>
        /// <param name="underlyingDataObject">The underlying data object to wrap.</param>
        public OutlookDataObject(System.Windows.IDataObject underlyingDataObject)
        {
            //get the underlying dataobject and its ComType IDataObject interface to it
            this.underlyingDataObject = underlyingDataObject;
            this.comUnderlyingDataObject = (System.Runtime.InteropServices.ComTypes.IDataObject)this.underlyingDataObject;

            //get the internal ole dataobject and its GetDataFromHGLOBLAL so it can be called later
            FieldInfo innerDataField = this.underlyingDataObject.GetType().GetField("_innerData", BindingFlags.NonPublic | BindingFlags.Instance);
            this.oleUnderlyingDataObject = (System.Windows.IDataObject)innerDataField.GetValue(this.underlyingDataObject);
            this.getDataFromHGLOBLALMethod = this.oleUnderlyingDataObject.GetType().GetMethod("GetDataFromHGLOBLAL", BindingFlags.NonPublic | BindingFlags.Instance);
        }

        #endregion

        #region IDataObject Members

        /// <summary>
        /// Retrieves the data associated with the specified class type format.
        /// </summary>
        /// <param name="format">A <see cref="T:System.Type"></see> representing the format of the data to retrieve. See <see cref="T:System.Windows.Forms.DataFormats"></see> for predefined formats.</param>
        /// <returns>
        /// The data associated with the specified format, or null.
        /// </returns>
        public object GetData(Type format)
        {
            return this.GetData(format.FullName);
        }

        /// <summary>
        /// Retrieves the data associated with the specified data format.
        /// </summary>
        /// <param name="format">The format of the data to retrieve. See <see cref="T:System.Windows.Forms.DataFormats"></see> for predefined formats.</param>
        /// <returns>
        /// The data associated with the specified format, or null.
        /// </returns>
        public object GetData(string format)
        {
            return this.GetData(format, true);
        }

        /// <summary>
        /// Retrieves the data associated with the specified data format, using a Boolean to determine whether to convert the data to the format.
        /// </summary>
        /// <param name="format">The format of the data to retrieve. See <see cref="T:System.Windows.Forms.DataFormats"></see> for predefined formats.</param>
        /// <param name="autoConvert">true to convert the data to the specified format; otherwise, false.</param>
        /// <returns>
        /// The data associated with the specified format, or null.
        /// </returns>
        public object GetData(string format, bool autoConvert)
        {
            //handle the "FileGroupDescriptor" and "FileContents" format request in this class otherwise pass through to underlying IDataObject 
            switch (format)
            {
                case "FileGroupDescriptor":
                    //override the default handling of FileGroupDescriptor which returns a
                    //MemoryStream and instead return a string array of file names
                    IntPtr fileGroupDescriptorAPointer = IntPtr.Zero;
                    try
                    {
                        //use the underlying IDataObject to get the FileGroupDescriptor as a MemoryStream
                        MemoryStream fileGroupDescriptorStream = (MemoryStream)this.underlyingDataObject.GetData("FileGroupDescriptor", autoConvert);
                        byte[] fileGroupDescriptorBytes = new byte[fileGroupDescriptorStream.Length];
                        fileGroupDescriptorStream.Read(fileGroupDescriptorBytes, 0, fileGroupDescriptorBytes.Length);
                        fileGroupDescriptorStream.Close();

                        //copy the file group descriptor into unmanaged memory 
                        fileGroupDescriptorAPointer = Marshal.AllocHGlobal(fileGroupDescriptorBytes.Length);
                        Marshal.Copy(fileGroupDescriptorBytes, 0, fileGroupDescriptorAPointer, fileGroupDescriptorBytes.Length);

                        //marshal the unmanaged memory to to FILEGROUPDESCRIPTORA struct
                        object fileGroupDescriptorObject = Marshal.PtrToStructure(fileGroupDescriptorAPointer, typeof(NativeMethods.FILEGROUPDESCRIPTORA));
                        NativeMethods.FILEGROUPDESCRIPTORA fileGroupDescriptor = (NativeMethods.FILEGROUPDESCRIPTORA)fileGroupDescriptorObject;

                        //create a new array to store file names in of the number of items in the file group descriptor
                        string[] fileNames = new string[fileGroupDescriptor.cItems];

                        //get the pointer to the first file descriptor
                        IntPtr fileDescriptorPointer = (IntPtr)((int)fileGroupDescriptorAPointer + Marshal.SizeOf(fileGroupDescriptorAPointer));

                        //loop for the number of files acording to the file group descriptor
                        for (int fileDescriptorIndex = 0; fileDescriptorIndex < fileGroupDescriptor.cItems; fileDescriptorIndex++)
                        {
                            //marshal the pointer top the file descriptor as a FILEDESCRIPTORA struct and get the file name
                            NativeMethods.FILEDESCRIPTORA fileDescriptor = (NativeMethods.FILEDESCRIPTORA)Marshal.PtrToStructure(fileDescriptorPointer, typeof(NativeMethods.FILEDESCRIPTORA));
                            fileNames[fileDescriptorIndex] = fileDescriptor.cFileName;

                            //move the file descriptor pointer to the next file descriptor
                            fileDescriptorPointer = (IntPtr)((int)fileDescriptorPointer + Marshal.SizeOf(fileDescriptor));
                        }

                        //return the array of filenames
                        return fileNames;
                    }
                    finally
                    {
                        //free unmanaged memory pointer
                        Marshal.FreeHGlobal(fileGroupDescriptorAPointer);
                    }

                case "FileGroupDescriptorW":
                    //override the default handling of FileGroupDescriptorW which returns a
                    //MemoryStream and instead return a string array of file names
                    IntPtr fileGroupDescriptorWPointer = IntPtr.Zero;
                    try
                    {
                        //use the underlying IDataObject to get the FileGroupDescriptorW as a MemoryStream
                        MemoryStream fileGroupDescriptorStream = (MemoryStream)this.underlyingDataObject.GetData("FileGroupDescriptorW");
                        byte[] fileGroupDescriptorBytes = new byte[fileGroupDescriptorStream.Length];
                        fileGroupDescriptorStream.Read(fileGroupDescriptorBytes, 0, fileGroupDescriptorBytes.Length);
                        fileGroupDescriptorStream.Close();

                        //copy the file group descriptor into unmanaged memory
                        fileGroupDescriptorWPointer = Marshal.AllocHGlobal(fileGroupDescriptorBytes.Length);
                        Marshal.Copy(fileGroupDescriptorBytes, 0, fileGroupDescriptorWPointer, fileGroupDescriptorBytes.Length);

                        //marshal the unmanaged memory to to FILEGROUPDESCRIPTORW struct
                        object fileGroupDescriptorObject = Marshal.PtrToStructure(fileGroupDescriptorWPointer, typeof(NativeMethods.FILEGROUPDESCRIPTORW));
                        NativeMethods.FILEGROUPDESCRIPTORW fileGroupDescriptor = (NativeMethods.FILEGROUPDESCRIPTORW)fileGroupDescriptorObject;

                        //create a new array to store file names in of the number of items in the file group descriptor
                        string[] fileNames = new string[fileGroupDescriptor.cItems];

                        //get the pointer to the first file descriptor
                        IntPtr fileDescriptorPointer = (IntPtr)((int)fileGroupDescriptorWPointer + Marshal.SizeOf(fileGroupDescriptorWPointer));

                        //loop for the number of files acording to the file group descriptor
                        for (int fileDescriptorIndex = 0; fileDescriptorIndex < fileGroupDescriptor.cItems; fileDescriptorIndex++)
                        {
                            //marshal the pointer top the file descriptor as a FILEDESCRIPTORW struct and get the file name
                            NativeMethods.FILEDESCRIPTORW fileDescriptor = (NativeMethods.FILEDESCRIPTORW)Marshal.PtrToStructure(fileDescriptorPointer, typeof(NativeMethods.FILEDESCRIPTORW));
                            fileNames[fileDescriptorIndex] = fileDescriptor.cFileName;

                            //move the file descriptor pointer to the next file descriptor
                            fileDescriptorPointer = (IntPtr)((int)fileDescriptorPointer + Marshal.SizeOf(fileDescriptor));
                        }

                        //return the array of filenames
                        return fileNames;
                    }
                    finally
                    {
                        //free unmanaged memory pointer
                        Marshal.FreeHGlobal(fileGroupDescriptorWPointer);
                    }

                case "FileContents":
                    //override the default handling of FileContents which returns the
                    //contents of the first file as a memory stream and instead return
                    //a array of MemoryStreams containing the data to each file dropped

                    //get the array of filenames which lets us know how many file contents exist
                    string[] fileContentNames = (string[])this.GetData("FileGroupDescriptor");

                    //create a MemoryStream array to store the file contents
                    MemoryStream[] fileContents = new MemoryStream[fileContentNames.Length];

                    //loop for the number of files acording to the file names
                    for (int fileIndex = 0; fileIndex < fileContentNames.Length; fileIndex++)
                    {
                        //get the data at the file index and store in array
                        fileContents[fileIndex] = this.GetData(format, fileIndex);
                    }

                    //return array of MemoryStreams containing file contents
                    return fileContents;
            }

            //use underlying IDataObject to handle getting of data
            return this.underlyingDataObject.GetData(format, autoConvert);
        }

        /// <summary>
        /// Retrieves the data associated with the specified data format at the specified index.
        /// </summary>
        /// <param name="format">The format of the data to retrieve. See <see cref="T:System.Windows.Forms.DataFormats"></see> for predefined formats.</param>
        /// <param name="index">The index of the data to retrieve.</param>
        /// <returns>
        /// A <see cref="MemoryStream"/> containing the raw data for the specified data format at the specified index.
        /// </returns>
        public MemoryStream GetData(string format, int index)
        {
            //create a FORMATETC struct to request the data with
            FORMATETC formatetc = new FORMATETC();
            formatetc.cfFormat = (short)DataFormats.GetDataFormat(format).Id;
            formatetc.dwAspect = DVASPECT.DVASPECT_CONTENT;
            formatetc.lindex = index;
            formatetc.ptd = new IntPtr(0);
            formatetc.tymed = TYMED.TYMED_ISTREAM | TYMED.TYMED_ISTORAGE | TYMED.TYMED_HGLOBAL;

            //create STGMEDIUM to output request results into
            STGMEDIUM medium = new STGMEDIUM();

            //using the Com IDataObject interface get the data using the defined FORMATETC
            this.comUnderlyingDataObject.GetData(ref formatetc, out medium);

            //retrieve the data depending on the returned store type
            switch (medium.tymed)
            {
                case TYMED.TYMED_ISTORAGE:
                    //to handle a IStorage it needs to be written into a second unmanaged
                    //memory mapped storage and then the data can be read from memory into
                    //a managed byte and returned as a MemoryStream

                    NativeMethods.IStorage iStorage = null;
                    NativeMethods.IStorage iStorage2 = null;
                    NativeMethods.ILockBytes iLockBytes = null;
                    System.Runtime.InteropServices.ComTypes.STATSTG iLockBytesStat;
                    try
                    {
                        //marshal the returned pointer to a IStorage object
                        iStorage = (NativeMethods.IStorage)Marshal.GetObjectForIUnknown(medium.unionmember);
                        Marshal.Release(medium.unionmember);

                        //create a ILockBytes (unmanaged byte array) and then create a IStorage using the byte array as a backing store
                        iLockBytes = NativeMethods.CreateILockBytesOnHGlobal(IntPtr.Zero, true);
                        iStorage2 = NativeMethods.StgCreateDocfileOnILockBytes(iLockBytes, 0x00001012, 0);

                        //copy the returned IStorage into the new IStorage
                        iStorage.CopyTo(0, null, IntPtr.Zero, iStorage2);
                        iLockBytes.Flush();
                        iStorage2.Commit(0);

                        //get the STATSTG of the ILockBytes to determine how many bytes were written to it
                        iLockBytesStat = new System.Runtime.InteropServices.ComTypes.STATSTG();
                        iLockBytes.Stat(out iLockBytesStat, 1);
                        int iLockBytesSize = (int)iLockBytesStat.cbSize;

                        //read the data from the ILockBytes (unmanaged byte array) into a managed byte array
                        byte[] iLockBytesContent = new byte[iLockBytesSize];
                        iLockBytes.ReadAt(0, iLockBytesContent, iLockBytesContent.Length, null);

                        //wrapped the managed byte array into a memory stream and return it
                        return new MemoryStream(iLockBytesContent);
                    }
                    finally
                    {
                        //release all unmanaged objects
                        Marshal.ReleaseComObject(iStorage2);
                        Marshal.ReleaseComObject(iLockBytes);
                        Marshal.ReleaseComObject(iStorage);
                    }

                case TYMED.TYMED_ISTREAM:
                    //to handle a IStream it needs to be read into a managed byte and
                    //returned as a MemoryStream

                    IStream iStream = null;
                    System.Runtime.InteropServices.ComTypes.STATSTG iStreamStat;
                    try
                    {
                        //marshal the returned pointer to a IStream object
                        iStream = (IStream)Marshal.GetObjectForIUnknown(medium.unionmember);
                        Marshal.Release(medium.unionmember);

                        //get the STATSTG of the IStream to determine how many bytes are in it
                        iStreamStat = new System.Runtime.InteropServices.ComTypes.STATSTG();
                        iStream.Stat(out iStreamStat, 0);
                        int iStreamSize = (int)iStreamStat.cbSize;

                        //read the data from the IStream into a managed byte array
                        byte[] iStreamContent = new byte[iStreamSize];
                        iStream.Read(iStreamContent, iStreamContent.Length, IntPtr.Zero);

                        //wrapped the managed byte array into a memory stream and return it
                        return new MemoryStream(iStreamContent);
                    }
                    finally
                    {
                        //release all unmanaged objects
                        Marshal.ReleaseComObject(iStream);
                    }

                case TYMED.TYMED_HGLOBAL:
                    //to handle a HGlobal the exisitng "GetDataFromHGLOBLAL" method is invoked via
                    //reflection

                    return (MemoryStream)this.getDataFromHGLOBLALMethod.Invoke(this.oleUnderlyingDataObject, new object[] { DataFormats.GetDataFormat((short)formatetc.cfFormat).Name, medium.unionmember });
            }

            return null;
        }

        /// <summary>
        /// Determines whether data stored in this instance is associated with, or can be converted to, the specified format.
        /// </summary>
        /// <param name="format">A <see cref="T:System.Type"></see> representing the format for which to check. See <see cref="T:System.Windows.Forms.DataFormats"></see> for predefined formats.</param>
        /// <returns>
        /// true if data stored in this instance is associated with, or can be converted to, the specified format; otherwise, false.
        /// </returns>
        public bool GetDataPresent(Type format)
        {
            return this.underlyingDataObject.GetDataPresent(format);
        }

        /// <summary>
        /// Determines whether data stored in this instance is associated with, or can be converted to, the specified format.
        /// </summary>
        /// <param name="format">The format for which to check. See <see cref="T:System.Windows.Forms.DataFormats"></see> for predefined formats.</param>
        /// <returns>
        /// true if data stored in this instance is associated with, or can be converted to, the specified format; otherwise false.
        /// </returns>
        public bool GetDataPresent(string format)
        {
            return this.underlyingDataObject.GetDataPresent(format);
        }

        /// <summary>
        /// Determines whether data stored in this instance is associated with the specified format, using a Boolean value to determine whether to convert the data to the format.
        /// </summary>
        /// <param name="format">The format for which to check. See <see cref="T:System.Windows.Forms.DataFormats"></see> for predefined formats.</param>
        /// <param name="autoConvert">true to determine whether data stored in this instance can be converted to the specified format; false to check whether the data is in the specified format.</param>
        /// <returns>
        /// true if the data is in, or can be converted to, the specified format; otherwise, false.
        /// </returns>
        public bool GetDataPresent(string format, bool autoConvert)
        {
            return this.underlyingDataObject.GetDataPresent(format, autoConvert);
        }

        /// <summary>
        /// Returns a list of all formats that data stored in this instance is associated with or can be converted to.
        /// </summary>
        /// <returns>
        /// An array of the names that represents a list of all formats that are supported by the data stored in this object.
        /// </returns>
        public string[] GetFormats()
        {
            return this.underlyingDataObject.GetFormats();
        }

        /// <summary>
        /// Gets a list of all formats that data stored in this instance is associated with or can be converted to, using a Boolean value to determine whether to retrieve all formats that the data can be converted to or only native data formats.
        /// </summary>
        /// <param name="autoConvert">true to retrieve all formats that data stored in this instance is associated with or can be converted to; false to retrieve only native data formats.</param>
        /// <returns>
        /// An array of the names that represents a list of all formats that are supported by the data stored in this object.
        /// </returns>
        public string[] GetFormats(bool autoConvert)
        {
            return this.underlyingDataObject.GetFormats(autoConvert);
        }

        /// <summary>
        /// Stores the specified data in this instance, using the class of the data for the format.
        /// </summary>
        /// <param name="data">The data to store.</param>
        public void SetData(object data)
        {
            this.underlyingDataObject.SetData(data);
        }

        /// <summary>
        /// Stores the specified data and its associated class type in this instance.
        /// </summary>
        /// <param name="format">A <see cref="T:System.Type"></see> representing the format associated with the data. See <see cref="T:System.Windows.Forms.DataFormats"></see> for predefined formats.</param>
        /// <param name="data">The data to store.</param>
        public void SetData(Type format, object data)
        {
            this.underlyingDataObject.SetData(format, data);
        }

        /// <summary>
        /// Stores the specified data and its associated format in this instance.
        /// </summary>
        /// <param name="format">The format associated with the data. See <see cref="T:System.Windows.Forms.DataFormats"></see> for predefined formats.</param>
        /// <param name="data">The data to store.</param>
        public void SetData(string format, object data)
        {
            this.underlyingDataObject.SetData(format, data);
        }

        /// <summary>
        /// Stores the specified data and its associated format in this instance, using a Boolean value to specify whether the data can be converted to another format.
        /// </summary>
        /// <param name="format">The format associated with the data. See <see cref="T:System.Windows.Forms.DataFormats"></see> for predefined formats.</param>
        /// <param name="autoConvert">true to allow the data to be converted to another format; otherwise, false.</param>
        /// <param name="data">The data to store.</param>
        public void SetData(string format, object data, bool autoConvert)
        {
            this.underlyingDataObject.SetData(format, data, autoConvert);
        }

        #endregion
    }
}



using System.IO;
using System.Windows;

namespace OutlookDragDropTestWPF
{
    /// <summary>
    /// Interaktionslogik für MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_DragEnter(object sender, DragEventArgs e)
        {
            //display formats available
            label1.Content = "Formats:\n";
            foreach (string format in e.Data.GetFormats())
            {
                label1.Content += "    " + format + "\n";
            }

            //ensure FileGroupDescriptor is present before allowing drop
            if (e.Data.GetDataPresent("FileGroupDescriptor"))
            {
                e.Effects = DragDropEffects.All;
            }
        }

        private void Window_Drop(object sender, DragEventArgs e)
        {
            //wrap standard IDataObject in OutlookDataObject
            OutlookDataObject dataObject = new OutlookDataObject(e.Data);

            //get the names and data streams of the files dropped
            string[] filenames = (string[])dataObject.GetData("FileGroupDescriptorW");
            MemoryStream[] filestreams = (MemoryStream[])dataObject.GetData("FileContents");

            label1.Content += "Files:\n";
            for (int fileIndex = 0; fileIndex < filenames.Length; fileIndex++)
            {
                //use the fileindex to get the name and data stream
                string filename = filenames[fileIndex];
                MemoryStream filestream = filestreams[fileIndex];
                label1.Content += "    " + filename + "\n";

                //save the file stream using its name to the application path
                FileStream outputStream = File.Create(filename);
                filestream.WriteTo(outputStream);
                outputStream.Close();
            }
        }
    }
}

28.11.2014 - 16:50 Uhr

Ah Ok, wenn Frame1 und Page1 selbst immer am Rand "anschlagen" soll, dann
muss der ScrollViewer um Frame1 weg wie witte schreibt und du musst ihn innerhalb jeder Page setzen und dann schauen, ob dir das verhalten dann passt.

28.11.2014 - 12:15 Uhr

Ich versuchs grad noch bisschen besser zu verstehen.

Der Frame1 in dem sich Page1 befindet soll immer nur so groß sein,
wie die aktuelle verfügbare Größe innerhalb des Tabcontrols richtig?

Wenn ja dann verstehe ich im Moment noch nicht ganz, warum sich Frame1 in einem ScrollViewer befindet.

Soll nur der Inhalt in Page1 scrollbar sein?

28.11.2014 - 12:05 Uhr

Hi Christoph1972,

danke für den Link. Ich hatte dieses Beispiel gestern auch schon mal gefunden,
aber es funktionierte nicht.

Ich hab mich aber jetzt noch mal dran gesetzt und geschaut woran es lag.
Ich musste das Beispiel noch mal etwas umbauen, damit es tut, aber jetzt funktioniert es einwandfrei! 😃

Ich musste den Code von der Seite:


    private void comboBox1_KeyDown(object sender, KeyEventArgs e)
    {
        ComboBox cb = sender as ComboBox;
        if (e.Key == Key.Tab && cb.IsDropDownOpen)
        {
            ComboBoxItem item = FocusManager.GetFocusedElement(Window.GetWindow(this)) as ComboBoxItem;
            cb.SelectedItem = item;
            cb.IsDropDownOpen = false;
            e.Handled = true;
        }
    }

   private void comboBox1_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
        {
            ComboBox cb = sender as ComboBox;
            cb.IsDropDownOpen = true;
        }

Bei mir umwandeln in:


        private void ComboBox_KeyDown(object sender, KeyEventArgs e)
        {
            ComboBox comboBox = sender as ComboBox;

            if (e.Key == Key.Tab && comboBox.IsDropDownOpen)
            {
                ComboBoxItem comboBoxItem = FocusManager.GetFocusedElement(Window.GetWindow(this)) as ComboBoxItem;
                comboBoxItem.IsSelected = true;
                comboBox.IsDropDownOpen = false;
                e.Handled = true;

                comboBox.MoveFocus(new TraversalRequest(FocusNavigationDirection.Right));
            }
        }

        private void ComboBox_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
        {
            comboBoxGotKeyboardFocusCounter += 1;

            if (comboBoxGotKeyboardFocusCounter == 1)
            {
                ComboBox comboBox = sender as ComboBox;
                comboBox.IsDropDownOpen = true;
            }
            else
            {
                comboBoxGotKeyboardFocusCounter = 0;
            }
        }

Der int Counter muss komischerweise sein. Damit der Focus nur gesetzt wird,
wenn GotKeyboardFocus zum ersten mal eintritt.
Beim zweiten mal soll er ja wegspringen.
Auf null muss ich ihn dann wieder setzen, weil alle nachfolgenden Comboboxen
an die selben Methoden gebunden sind.

Danke noch mal für die Hilfe!

27.11.2014 - 21:09 Uhr

Hi Christoph1972,

danke für deine Antwort! Hörte sich einleuchtend an, aber hat leider nicht funktioniert! 😦

So hab ich deine Idee umgesetzt . . .


        private void ComboBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Tab)
            {
                ((ComboBox)sender).Focusable = false;

                TraversalRequest traversalRequest = new TraversalRequest(FocusNavigationDirection.Next);
                MoveFocus(traversalRequest);

                ((ComboBox)sender).Focusable = true;
            }
        }

Die Combobox springt wieder aus sich selbst raus und dann wieder in sich selbst rein.

Wenn ich mit den Pfeiltasten in der Combobox zu einem Item navigiere und dann die Entertaste drücke, wird das Item zum SelectedItem und die Combobox schließt sich. Dann kann ich auch mit der Tabtaste zur nächsten Combobox weiter.

Gibts vielleicht ne Möglichkeit dieses "automatische Entertastenverhalten" irgendwo einzusehen?
Dann könnte ich den Code in der Methode anwenden und um MoveFocus erweitern.

27.11.2014 - 16:18 Uhr

Hallo adm1n,

zunächst würde ich die folgenden 3 Dinge aus dem TabControl herausnehmen:

ScrollViewer.CanContentScroll="True"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto"

und dann würde ich im TabItem.Content den ScrollViewer in einen Border packen.

Was pssiert dann?

27.11.2014 - 12:11 Uhr

Hallo Zusammen,

ich habe in meiner Anwendung einen Bereich der in etwa so aussieht wie der auf dem angehängten Bild.

Startend von der TextBox möchte ich per Tab-Taste immer in die Folgende Combobox, die sich bei erhalt des Focus öffnen soll.
Das funktioniert einwandfrei, aber nur so lange ich zB.: per Pfeiltasten nicht in die Items der Combobox reinspringe.
Wenn ich dann die Tabtaste drücke navigiere ich damit nur innerhalb der offenen Combobox und springe nicht mehr zur nächsten, was ich aber eigentlich will.

Ich habe in der Codebehind meiner View jetzt schon folgendes probiert:


private void ComboBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Tab)
            {
                TraversalRequest traversalRequest = new TraversalRequest(FocusNavigationDirection.Next);
                MoveFocus(traversalRequest);
            }
        }

Das greift auch wunderbar ABER, das Ergebnis ist, dass sich die Combobox schließt und danach nicht die nächste, sondern wieder die selbe aufgeht.

Wahrscheinlich weil der Aufruf aus dem Popup der Combobox kommt und mit FocusNavigationDirection.Next dann das Elternelement also eben die Combobox selbst wieder den Focus hat.

Ich bräuchte also Sinnbildlich gesprochen eine FocusNavigationDirection.Next => Next Funktion.

Hat evtl. jemand von euch eine Idee, wie ich das Problem lösen könnte?

Vielen Dank und viele Grüße
Torsten

14.11.2014 - 14:42 Uhr

Hallo Witte,

Danke für deine Antwort!

Mit RelativeSource hatte ich auch schon unzählige Beispiele gefunden, aber die funktionieren bei mir alle nicht.
Ich hab es inzwischen zumindest zum laufen gebracht, in dem ich mein Command im ViewModel auf static gesetzt habe und das Commandbindung dann wie folgt.


                                 <DataGridTemplateColumn Header="CheckBoxSpalte1">
                                     <DataGridTemplateColumn.CellTemplate>
                                         <DataTemplate>
                                             <CheckBox IsChecked="{Binding MeinObjektBool1, UpdateSourceTrigger=PropertyChanged}"
                                                       Command="{x:Static vm:MeinViewModel.TestCommand}"></CheckBox>
                                         </DataTemplate>
                                     </DataGridTemplateColumn.CellTemplate>
                                     <DataGridTemplateColumn.CellEditingTemplate>
                                         <DataTemplate>
                                                 <CheckBox IsChecked="{Binding MeinObjektBool1, UpdateSourceTrigger=PropertyChanged}">
                                           </CheckBox>
                                         </DataTemplate>
                                     </DataGridTemplateColumn.CellEditingTemplate>
                                 </DataGridTemplateColumn>

Und zu deiner Frage, die Aktion in der Checkbox ist ja eigentlich die IsChecked Eigenschaft und die greift ja auch direkt auf das selektierte Objekt. Ich will nur parallel zum Klick etwas anderes auslösen und dafür brauchte ich die Bindung an mein Command, oder hab ich dich jetzt falsch verstanden?

14.11.2014 - 13:47 Uhr

Update:

Zumindest weiß ich jetzt schon mal warum es nicht funktioniert.
Meine View ist über


d:DataContext="{d:DesignInstance {x:Type vm:MeinViewModel}}"

an das ViewModel gebunden.

Dadurch haben alle Elemente innerhalb meiner View zugriff auf alle
im ViewModel veröffentlichten Objekt, ObjektCollections und Commands.

Dadurch dass das DatGrid aber in seiner ItemsSource auf eine bestimmte
Collection innerhalb des ViewModels bindet, haben alle Elemente
innerhalb des DataGrids nur noch Zugriff auf die Collection und nicht mehr
auf das ganze ViewModel.

Und wie breche ich jetzt über die Commandeigenschaft der Ceckboxen aus
diesem "Gefängnis" wieder aus?

14.11.2014 - 10:59 Uhr

Hallo Zusammen,

ich nutze in meiner Anwendung Checkboxen und in einem DataGrid ChackCoxColumns.

Die Checkboxen lösen einwandfrei Commands in meinem ViewModel aus.
Zwei Varianten funktionieren absolut Perfekt.

Variante 1:


			                <CheckBox Content="CheckBox1"
                                      IsChecked="{Binding _MeineBoolscheEigenschaft1, UpdateSourceTrigger=PropertyChanged}"
                                      Command="{Binding MeinCommand1}">
                            </CheckBox>

Variante 2:


                            <CheckBox Content="CheckBox2"
                                      IsChecked="{Binding _MeineBoolscheEigenschaft2, UpdateSourceTrigger=PropertyChanged}">
                                <i:Interaction.Triggers>
                                    <i:EventTrigger EventName="Click">
                                        <i:InvokeCommandAction Command="{Binding MeinCommand2}">
                                        </i:InvokeCommandAction>
                                    </i:EventTrigger>
                                </i:Interaction.Triggers>
                            </CheckBox>

Ich habe beide Varianten in einem DataGrid probiert . . .


                           <DataGrid ItemsSource="{Binding _MeineObjektCollection, UpdateSourceTrigger=PropertyChanged}"
                                  AutoGenerateColumns="False"
                                  CanUserAddRows="False" 
                                  CanUserDeleteRows="False" 
                                  SelectionMode="Single" 
                                  SelectionUnit="FullRow">

                            <DataGrid.Columns>
                                
                                <DataGridTemplateColumn Header="CheckBoxSpalte1">
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>
                                            <CheckBox IsChecked="{Binding MeinObjektBool1, UpdateSourceTrigger=PropertyChanged}"></CheckBox>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>
                                    <DataGridTemplateColumn.CellEditingTemplate>
                                        <DataTemplate>
                                                <CheckBox IsChecked="{Binding MeinObjektBool1, UpdateSourceTrigger=PropertyChanged}">
                                                    <i:Interaction.Triggers>
                                                        <i:EventTrigger EventName="Click">
                                                            <i:InvokeCommandAction Command="{Binding MeinCommand3}">
                                                        </i:EventTrigger>
                                                    </i:Interaction.Triggers>
                                                </CheckBox>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellEditingTemplate>
                                </DataGridTemplateColumn>

				                <DataGridTemplateColumn Header="CheckBoxSpalte2">
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>
                                            <CheckBox IsChecked="{Binding MeinObjektBool2, UpdateSourceTrigger=PropertyChanged}"></CheckBox>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>
                                    <DataGridTemplateColumn.CellEditingTemplate>
                                        <DataTemplate>
                                                <CheckBox IsChecked="{Binding MeinObjektBool2, UpdateSourceTrigger=PropertyChanged}"
                                                                 Command="{Binding MeinCommand4}">
                                                </CheckBox>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellEditingTemplate>
                                </DataGridTemplateColumn>

                            </DataGrid.Columns>

                        </DataGrid>

. . . aber weder die Eine, noch die Andere funktioniert.

Hat jemand evtl. eine Idee woran das liegt und wie ich es schaffe das Command auszulösen?

Danke Euch schon mal!

05.11.2014 - 16:08 Uhr

Hallo Zusammen,

ich habe ein DataGrid in meiner View dessen ItemsSource an eine ObservableCollection im ViewModel gebunden ist.

Ich möchte nicht, dass beim Start oder nach dem Aktualisieren die
erste Zeile markiert ist.

Weiß jemand wo ich diesen "Automatismus" abschalten kann?

Vielen Dank und viele Grüße
Torsten

26.09.2014 - 14:01 Uhr

Hallo witte,

vielen Dank für deine Antwort!
Da mein Server ein SQL Server 2008 R2 ist, hat sich dass dann laut deinem Link ja leider erledigt.

Vielleicht probiere ich den letzten Tipp mit der Zeitmessung mal aus, der dort angegeben ist.

Danke noch mal.

26.09.2014 - 12:56 Uhr

Hallo Zusammen,

ich stehe gerade vor einem Problem, bei dem ich nicht mal weiß, wie ich es überhaupt google'n sollte.

Folgende vereinfachte Anwendung liegt vor (Client/Server).

Im Server Programmteil gibt es ne Linq Abfrage zum SQL Server (Datenzugriffsschicht) z.Bsp.:


public List<TestDaten> GetTestDaten()
{
     var query = from t in Context.TestDaten
                 select t;

     return query.ToList();
}

Danach kommt die Geschäftslogik, die an die Datenzugriffschicht durchreicht


public List<TestDaten> GetTestDaten()
        {
            return TestDatenManager.GetTestDaten();
        }

Dann die Nachrichtenklassen . . .


[MessageContract]
    public class GetTestDatenAnfrage
    {

    }

    [MessageContract]
    public class GetTestDatenAntwort
    {
        [MessageBodyMember]
        public List<TestDaten> GetTestDaten{ get; set; }
    }

Die Dienstschnittstellen . . .


[OperationContract]
        GetTestDatenAntwort GetTestDaten(GetTestDatenAnfrage Anfrage);

Der Dienst . . .


public GetTestDatenAntwort GetTestDaten(GetTestDatenAnfrage Anfrage)
        {
            return new GetTestDatenAntwort()
            {
                GetTestDaten = tdm.GetTestDaten()
            };
        }

Das ViewModel . . .


private ObservableCollection<TestDaten> _testDaten { get; set; }
public ObservableCollection<TestDaten> _TestDaten
        {
            get 
            { 
                return _testDaten; 
            }
            set 
            { 
                _testDaten= value;
                this.OnPropertyChanged("_TestDaten"); 
            }
        }

//Später im Code . . .

private void TestDatenHolen()
{
     DienstClient client = new DienstClient();

     try
     {
          _TestDaten = client.GetTestDaten();
          client.Close();
     }

     catch (Exception ex)
     {
          irgendWasDasDenBlödenFehlerAnzeigt = ex.Message;
          client.Close();
     }
}

Und die View . . .


<DataGrid ItemsSource="{Binding _TestDaten}">
</DataGrid>

Nehmen wir mal an die Testdaten sind Groß und brauchen ne weile bis sie geladen sind.

Gibt es in einem solchen Szenario überhaupt die Möglichkeit den User über den Fortschritt in % zu informieren?

Weil so wie ich das sehe, sind die Aufgaben ja geteilt und werden immer wieder an jemand anderen weitergeleitet, der dann darauf wartet, dass zurück geliefert wird.

Kann man in diesen "Durchreichevorgang" überhaupt "reinschauen", um in Erfahrung zu bringen, wie weit die Gesamtaufgabe Fortgeschritten ist?

Vielen Dank schon mal für euren Input!

11.04.2014 - 16:48 Uhr

Danke ProGamer!

so einfach kann es manchmal sein.


<TextBox Text="{Binding _Text, UpdateSourceTrigger=PropertyChanged}"/> 

11.04.2014 - 16:34 Uhr

Hallo Zusammen,

im ViewModel gibt es einen string namens "_Text".


        private string _text { get; set; }
        public string _Text
        {
            get { return _text; }
            set
            {
                _text = value; this.OnPropertyChanged("_Text");
            }
        }

In der View gibt es dann eine Textbox, die an den string gebunden ist.


<TextBox Text="{Binding _Text}"/>

Nun ist es wohl so, dass der in der Textbox eingegebene Text erst in den string geschrieben wird, wenn der Nutzer die Textbox verlässt.

Gibt es eine Möglichkeit nach jedem TextChanged (also nach jedem eingegebenen Zeichen) den string zu aktualisieren?

Danke und Gruß
Torsten

11.04.2014 - 13:49 Uhr

Hallo Christian,

Danke für deine Antwort.

Da ich mich um das Auslösen nicht selber kümmern muss, sondern nur darauf "Lauschen" muss, wann es ausgelöst wurde, wird es wohl auf das DelegateCommand herauslaufen.

Ich habe es jetzt zwar noch nicht umgesetzt, aber ich denke dass das die Lösung sein sollte.

Danke noch mal!

Gruß
Torsten

03.04.2014 - 15:29 Uhr

Hallo Mallet,

ja, ich muss anstelle von:


                    if (_Hersteller.ChangeTracker.State == ObjectState.Modified)
                     {
                         HerstellerAendernCommand.IsEnabled = true;
                     }

eher dass hier machen:


                     HerstellerAendernCommand.CanExecute(
                     //Code der überwacht, ob auf der HerstellerCollection CollectionChanged ausgelöst wurde
                                                         );

Derzeit weiß ich nur noch nicht wie ich das mache, weil ich von der HerstellerCollection nur das "Angebot" bekomme, das CollectionChanged Event auszulösen, aber keine Möglichkeit zu "lauschen", ob es ausglöst wurde.

Da muss ich mich noch ein bisschen schlau drüber machen.

Danke euch!

Gruß
Torsten

02.04.2014 - 15:00 Uhr

Sorry für den Augenschmerz MrSparkle! 😉

Das Herstellers kein gültiger Plural von Hersteller ist, ist mir schon klar.
Für mich im Code sehe ich dadurch aber schneller, ob ich mit der Collection, oder dem Objekt arbeite.

02.04.2014 - 14:56 Uhr

Hallo Mallett,

danke für deine Antwort.

Die Anforderung ist, dass der Button für das ganze DataGrid erlaubt ist.
Beim Start wird eine Liste von Herstellern geladen und angezeigt.
Der Status ist "unverändert", also soll der Button "Änderungen speichern"
inaktiv sein.

Sobald im DataGrid bei einem Hersteller ein Häkchen gesetzt oder entfernt wird, ist das eine Änderung die gespeichert werden kann und der Button soll aktiv werden.

02.04.2014 - 14:29 Uhr

Hallo Zusammen,

ich habe ein Collection von Herstellern:


        private ObservableCollection<ArtikelHersteller> _herstellers { get; set; }
        public ObservableCollection<ArtikelHersteller> _Herstellers
        {
            get { return _herstellers; }
            set { _herstellers = value; this.OnPropertyChanged("_Herstellers");
            }
        }

und den Hersteller selbst:


        private ArtikelHersteller _hersteller;
        public ArtikelHersteller _Hersteller
        {
            get { return _hersteller; }
            set { _hersteller = value; this.OnPropertyChanged("_Hersteller");

                    //Das hier funktioniert nur, wenn ich im DataGrid nach der Änderung
                    //erst einen anderen Hersteller auswähle und dann wieder auf den
                    //geänderten Hersteller zurück wechsle.
                    if (_Hersteller.ChangeTracker.State == ObjectState.Modified)
                    {
                        HerstellerAendernCommand.IsEnabled = true;
                    }
                }
        }

Die ItemsSource des DataGrids bindet auf "_HerstellersView" und SelectedItem bindet auf _Hersteller.

Die "_HerstellersView" beinhaltet die Collection "_Herstellers" und ist nur dazu da, damit ich LiveShaping verwenden kann und sich neu hinzugefügte Hersteller, automatisch in alphabetischer Reihenfolge im DataGrid platzieren.


                            <DataGrid ItemsSource="{Binding _HerstellersView}"
                                      SelectedItem="{Binding _Hersteller}"
                                      IsSynchronizedWithCurrentItem="True"
                                      AutoGenerateColumns="False" 
                                      CanUserAddRows="False" 
                                      CanUserDeleteRows="False" 
                                      SelectionMode="Single" 
                                      SelectionUnit="FullRow"
                                      x:Name="dgr_Hersteller">
                                <i:Interaction.Triggers>
                                    <i:EventTrigger EventName="SelectionChanged">
                                        <i:InvokeCommandAction Command="{Binding HerstellerSelectedItemChangedCommand}"
                                                               CommandParameter="{Binding ElementName=dgr_Hersteller, Path=SelectedItem.HerstellerID}"/>
                                    </i:EventTrigger>
                                </i:Interaction.Triggers>
                                <DataGrid.Columns>
                                    <!--Um diese CheckBoxColumn geht es bei meinem Problem-->
                                    <DataGridCheckBoxColumn Header="Inaktiv"
                                                            Binding="{Binding IstInaktiv}"
                                                            Width="Auto"/>
                                    <DataGridTextColumn Header="Hersteller"
                                                        Binding="{Binding HerstellerName}"
                                                        Width="Auto"
                                                        IsReadOnly="True"/>
                                </DataGrid.Columns>
                            </DataGrid>

Nun zu meinem Problem.

Unter dem DataGrid habe ich einen Button, der an das "HerstellerAendernCommand" bindet. Beim Start ist die "IsEnabled-Eigenschaft = false".

Nun möchte ich, dass wenn ich in der CheckBoxColumn "IstInaktiv" nen Haken setze oder entferne, dass dann das "HerstellerAendernCommand" IsEnabled = true ist.

Es funktioniert wenn ich nach dem setzen, oder entfernen eines Häkchens schnell einen anderen Hersteller auswähle und dann wieder auf den geänderten Hersteller zurück wechsle, aber diesen Schritt würde ich gerne umgehen.

Kann mir hier jemand weiter helfen?

Danke und Gruß
Torsten

21.03.2014 - 22:16 Uhr

Naja, die "Tablogik" ist mir schon bekannt. Stell dir vor du hast einen Tab in dem dir ein Datagrid mit einer Liste von Vorgängen angezeigt wird und du möchtest einen Vorgang öffnen und vielleicht noch einen weiteren, um diese beiden zum Beispiel zu vergleichen. Nur einen einzigen Workspace anzubieten fände ich da recht kompliziert.

21.03.2014 - 21:53 Uhr

ja genau so meine ich das und ne Treeview wäre mir auch noch lieber als die Buttons, aber das ist nicht so wichtig.

Du hättest also keine bedenken dabei, dass zum Beispiel neben dem "Buchungstab" aus der Anwendungsdomäne "Lagerverwaltung" der "Neue Rechnungtab" aus der Anwendungsdomäne "Rechnungswesen" geöffnet ist?

Ich fände es halt "aufgeräumter", wenn es für jede Anwendungsdomäne einen Tab gäbe unter dem dann die dazugehörigen Workspaces je nach bedarf auftauchen.

Ich hab halt bisschen Angst, dass es ansonsten irgendwann mal haufenweise Tabs sein könnten, die auf der selben Ebene nebeneinander offen sind und die dann aus unterschiedlichen Anwendungsdomänen stammen.

Wenn das aber durchaus üblich sein sollte, dass man es nicht weiter verschachtelt, dann würde ich mich jetzt auch von dem Gedanken trennen und die Struktur einfach nur im linken Menü weiter verfolgen.

21.03.2014 - 20:59 Uhr

Der Sinn der Beispielanwendung ist mir eigentlich ziemlich Egal, also ich meine die Bereiche der Views die darin vorkommen. Es geht mir hier weder um AlleCustomer noch um AddCustomer. Die Views bestimme ich ja in meiner Anwendung später selbst.

Vielmehr geht es mir um die Aufteilung. Du kannst halt auf der Linken Seite dein komplette Menü haben und das eben Auch noch nach Anwendungsbereichen Strukturiert.

Ich will auch keine SubWorkspaces reinfrickeln, denn die habe ich ja schon. Was ich brauche sind Tabs über den SubWorkspaces die diese eben Kategorisieren. Nehmen wir einfach an, dass "Lagerverwaltung" und "Bestellsystem" eigentlich zwei eigene Anwendungen sein würden, die ich aber in einer einzigen Anwendung zusammen laufen lassen kann.

Die Button auf der linken Seite holen ja bei mir grundsätzlich nur Subworkspaces nur müssen die eben wissen unter welchem "Übertap" sie sich eingliedern müssen und sie müssen wissen, ob der "Übertap" überhaupt schon offen ist, damit sie in dem Fall, in dem ich den jeweils ersten SubWorkspace hole den "Übertap" auch erzeugen können, um sich danach darunter einreihen zu können.

Vielleicht drücke ich mich auch einfach zu unverständlich aus, denn ich für meinen Teil fände eine solche Aufteilung schon recht praktisch.

Man könnte für ein Unternehmen eine Anwendung schreiben in der Alle Anwendungen drin sind. Man könnte ja auf der linken Seite dann einfach einen neuen Bereich "Rechnungswesen" hinzufügen und darunter erstellen sich die Button wieder selbst (was weiß ich . . . "Neue Rechnung", "Rechnung suchen", "Rechnungsübersicht", etc.) .

21.03.2014 - 17:45 Uhr

und wieder einmal melde ich mich zurück . . .

Im Moment sehe ich glaube ich gerade den Wald vor lauter Bäumen nicht mehr.

Im ursprünglichen MSDN Beispiel gibt es ja nur die eine Ebene Workspaces, die wie folgt aussieht:


//In MainWindowViewModel . . .

ObservableCollection<WorkspaceViewModel> _workspaces;

#region Workspaces
        
        public ObservableCollection<WorkspaceViewModel> Workspaces
        {
            get
            {
                if (_workspaces == null)
                {
                    _workspaces = new ObservableCollection<WorkspaceViewModel>();
                    _workspaces.CollectionChanged += this.OnWorkspacesChanged;
                }

                return _workspaces;
            }
        }

        void OnWorkspacesChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null && e.NewItems.Count != 0)
            {
                foreach (WorkspaceViewModel workspaces in e.NewItems)
                {
                    workspaces.RequestClose += this.OnWorkspacesRequestClose;
                }
            }

            if (e.OldItems != null && e.OldItems.Count != 0)
            {
                foreach (WorkspaceViewModel workspaces in e.OldItems)
                {
                    workspaces.RequestClose -= this.OnWorkspacesRequestClose;
                }
            }
        }

        void OnWorkspacesRequestClose(object sender, EventArgs e)
        {
            WorkspaceViewModel workspace = sender as WorkspaceViewModel;
            workspace.Dispose();
            this.Workspaces.Remove(workspace);
        }

#endregion Workspaces


Als kurzes Beispiel jetzt nur der "Add" der Lagerbestandsübersicht:


void ShowLagerbestand()
{            
          LagerbestandsuebersichtViewModel workspace = 
                this.Workspaces.FirstOrDefault(vm => vm is LagerbestandsuebersichtViewModel)
                as LagerbestandsuebersichtViewModel;

            if (workspace == null)
            {
                workspace = new LagerbestandsuebersichtViewModel();
                this.Workspaces.Add(workspace);
            }

            this.SetActiveWorkspace(workspace);
}

Jetzt habe ich mir gedacht, dass ich das ganze Konstrukt natürlich erweitern muss, weil ich sonst ja keine "Ebenen" hinkriege und das stelle ich mir so vor:


ObservableCollection<WorkspaceViewModel> _mainWorkspaces;
ObservableCollection<WorkspaceViewModel> _subWorkspacesLagerverwaltung;
ObservableCollection<WorkspaceViewModel> _subWorkspacesBestellsystem;

#region MainWorkspaces
        
        public ObservableCollection<WorkspaceViewModel> MainWorkspaces
        {
            get
            {
                if (_mainWorkspaces == null)
                {
                    _mainWorkspaces = new ObservableCollection<WorkspaceViewModel>();
                    _mainWorkspaces.CollectionChanged += this.OnMainWorkspacesChanged;
                }

                return _mainWorkspaces;
            }
        }

        void OnMainWorkspacesChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null && e.NewItems.Count != 0)
            {
                foreach (WorkspaceViewModel mainWorkspaces in e.NewItems)
                {
                    mainWorkspaces.RequestClose += this.OnMainWorkspacesRequestClose;
                }
            }

            if (e.OldItems != null && e.OldItems.Count != 0)
            {
                foreach (WorkspaceViewModel mainWorkspaces in e.OldItems)
                {
                    mainWorkspaces.RequestClose -= this.OnMainWorkspacesRequestClose;
                }
            }
        }

        void OnMainWorkspacesRequestClose(object sender, EventArgs e)
        {
            WorkspaceViewModel workspace = sender as WorkspaceViewModel;
            workspace.Dispose();
            this.MainWorkspaces.Remove(workspace);
        }

#endregion

#region SubWorkspacesLagerverwaltung

        public ObservableCollection<WorkspaceViewModel> SubWorkspacesLagerverwaltung
        {
            get
            {
                if (_subWorkspacesLagerverwaltung == null)
                {
                    _subWorkspacesLagerverwaltung = new ObservableCollection<WorkspaceViewModel>();
                    _subWorkspacesLagerverwaltung.CollectionChanged += this.OnSubWorkspacesLagerverwaltungChanged;
                }

                return _subWorkspacesLagerverwaltung;
            }
        }

        void OnSubWorkspacesLagerverwaltungChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null && e.NewItems.Count != 0)
            {
                foreach (WorkspaceViewModel subWorkspacesLagerverwaltung in e.NewItems)
                {
                    subWorkspacesLagerverwaltung.RequestClose += this.OnSubWorkspacesLagerverwaltungRequestClose;
                }
            }

            if (e.OldItems != null && e.OldItems.Count != 0)
            {
                foreach (WorkspaceViewModel subWorkspacesLagerverwaltung in e.OldItems)
                {
                    subWorkspacesLagerverwaltung.RequestClose -= this.OnSubWorkspacesLagerverwaltungRequestClose;
                }
            }
        }

        private void OnSubWorkspacesLagerverwaltungRequestClose(object sender, EventArgs e)
        {
            WorkspaceViewModel subWorkspace = sender as WorkspaceViewModel;
            subWorkspace.Dispose();
            this.SubWorkspacesLagerverwaltung.Remove(subWorkspace);
        }

#endregion

//danach dann natürlich auch noch mal alles für SubWorkspacesBestellsystem, 
//was ich mir hier jetzt spare, weil sonst alles zu viel wird


Und jetzt kommt mein Problem bei dem ich nicht mehr weiter komme.
Die Lagerbestandsübersicht muss ja jetzt nicht mehr zu Workspaces (was ja inzwischen die MainWorkspaces sind), sondern zu den SubWorkspacesLagerverwaltung, was dann so aussehen würde:


void ShowLagerbestand()
        {
            LagerbestandsuebersichtViewModel workspace = 
                this.SubWorkspacesLagerverwaltung.FirstOrDefault(vm => vm is LagerbestandsuebersichtViewModel)
                as LagerbestandsuebersichtViewModel;

            if (workspace == null)
            {
                workspace = new LagerbestandsuebersichtViewModel();
                this.SubWorkspacesLagerverwaltung.Add(workspace);
            }

            this.SetActiveWorkspace(workspace);
        }

Und ab hier habe ich keine Ahnung, wie ich die SubWorkspaceLagerverwaltung (was ja eine Collection ist) in die MainWorkspaces bekomme.

Rein vom Gedanken her müsste ich ja in der Methode schauen, ob die MainWorkspaces Collection leer ist oder nicht, dann müsste ich schauen, ob der "Zieltab" Lagerverwaltung geöffnet ist und wenn nicht, muss er halt geöffnet werden und dann muss unterhalb des Lagerverwaltungstabs ja die Collection von SubWorkspaceLagerverwaltung wieder in Form von Tabs angezeigt werden.

Ich glaub ich denk hier gerade viel zu kompliziert, aber ich komm einfach nicht auf die Lösung.

21.03.2014 - 13:23 Uhr

So jetzt hab ich mir das Thema mal angeschaut und auch das Video dazu und schon mal Danke dafür!

Dein "Einwand" hier kommt zur rechten Zeit, denn ich bin ja an der Stelle noch gar nicht wirklich angekommen, was du ja daran sehen kannst, dass ich überhaupt keine CustomerView habe wie im Beispiel, sondern eben erst mal nur die "DummyViews" mit je einem TextBlock.

Deine Vorgehensweise wirkt da schon echt besser, da auf die Art dann wirklich weniger Fehlermöglichkeiten vorhanden sind.

Danke dir noch mal für deinen Vorbehalt!

21.03.2014 - 12:02 Uhr

Nein, ich habe es noch nicht angeschaut. Ich muss gerade etwas anderes machen und komme wahrscheinlich erst im laufe des Nachmittages dazu, aber ich schau es mir auf jeden Fall an.

21.03.2014 - 11:39 Uhr

Hallo ErfinderDesRades,

Danke für deine Antwort.

Meine UserControls sehen schon so aus wie du sie beschreibst (siehe Beispielcode).

<UserControl x:Class="MeinMVVMTestProjekt.Views.LagerbestandsuebersichtView"
             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" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid Margin="20">

        <TextBlock Text="Lagerbestandsübersicht"
                   FontSize="26"
                   VerticalAlignment="Center"
                   HorizontalAlignment="Center"/>

    </Grid>
</UserControl>

Die TabControls befinden sich jeweils eine und zwei Ebenen über den UserControls.

21.03.2014 - 10:02 Uhr

Danke für deine Antwort Th69, da haben wir gerade wohl ziemlich gleichzeitig geantwortet! 😃

Jetzt habe ich auf alle Fälle schon mal zwei Lösungswege die ich probieren kann, falls einer nicht klappt! 😃

21.03.2014 - 09:57 Uhr

so . . . ich bin glaube ich wieder ein Stück weiter gekommen.

In der bisherigen Anwendung ist es ja so, dass ich in dem ich links auf einen Button klicke eigentlich einen "SubWorkspace" öffne, der sich aber von der Hierarchie her derzeit als "MainWorkspace" öffnet, weil es in der Beispielanwendung ja derzeit einfach nur "Workspaces" ohne Hierarchie gibt.

Innerhalb dieser erzeugten Tabs seht Ihr ja in den Screenshots einfach nur TextBlock-Elemente in denen noch mal das Gleiche wie im TabItemHeader steht. Das sind ganz banale UserControls die als Views dienen sollen und in denen eben jetzt erst mal nur ein TextBlock angezeigt wird, damit ich für mich sehen kann, dass auch überall die richtige View aufgerufen wird.

Und ich denke dass ich mit meinen "SubWorkspaces" genau in die Viewebene rein muss. Ich muss die View einfach noch ne Ebene wieter nach unten schieben und darüber dann die "SubWorkspaces".

Von denen muss es dann wieder zwei Gruppen geben, also zum Beispiel "LagerSubWorkspaces" und "BestellSubWorkspaces".
Diese Collections müssen dann vor der Erstellung prüfen, ob es in der Collection der "MainWorkspaces" das ElterenElement schon gibt und wenn nicht muss es eben hinzugefügt werden.

Ich werde das jetzt mal versuchen umzusetzen.

20.03.2014 - 21:56 Uhr

Hallo Th69,

Danke für deine Antwort, aber ich glaube dass ich mit dem ContentControl im ContentControl falsch liege.

Ich hab jetzt das Template vom TabControl mal manuell abgeändert:

<DataTemplate x:Key="WorkspacesTemplate">
        <TabControl Margin="5">
            <TabItem Header="Lagerverwaltung">
                <TabControl IsSynchronizedWithCurrentItem="True"
                            ItemsSource="{Binding}"
                            ItemTemplate="{StaticResource ClosableTabItemTemplate}"
                            Margin="5">
                </TabControl>
            </TabItem>
            <TabItem Header="Bestellsystem">                
            </TabItem>
        </TabControl>
    </DataTemplate>

Selbstverständlich ist das aber ziemlich sinnlos, denn es soll ja nicht hart im Code stehen sondern sich so aufbauen wie die "Untertabs".

Ich hab jetzt auch noch mal ein Bild nach der Änderung der Anwendung gemacht, damit man besser sehen kann, was ich eigentlich vor habe.

20.03.2014 - 20:46 Uhr

Hallo noch mal Zusammen,

ich komme der Lösung glaube ich langsam etwas näher.
Das TabControl unter "Arbeitsbereiche" sieht folgendermaßen aus:

<Border Grid.Column="2"
                    Margin="0 5 5 5">
                <HeaderedContentControl Content="{Binding Path=Workspaces}"
                                        ContentTemplate="{StaticResource WorkspacesTemplate}"
                                        Header="Arbeitsbereiche">
                </HeaderedContentControl>
            </Border>

Das HeaderedContentControl bindet an eine Collection von WorkspaceViewModels in MainWindowViewModel. Das ConetntTemplate ist ein TabControl das folgendermaßen aussieht.

    <DataTemplate x:Key="ClosableTabItemTemplate">
        <DockPanel>
            <Grid DockPanel.Dock="Top">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <Button Command="{Binding Path=CloseCommand}"
                        Content="X"
                        Cursor="Hand"
                        Grid.Column="1"
                        Focusable="False"
                        FontFamily="Courier"
                        FontSize="9"
                        FontWeight="Bold"
                        Margin="0 1 0 0"
                        Padding="0"
                        VerticalAlignment="Bottom"
                        Width="16"
                        Height="16"/>
                <ContentPresenter Content="{Binding Path=DisplayName}"
                                  VerticalAlignment="Center"
                                  Margin="3 0"/>
            </Grid>
        </DockPanel>
    </DataTemplate>
    
    <DataTemplate x:Key="WorkspacesTemplate">
        <TabControl IsSynchronizedWithCurrentItem="True"
                    ItemsSource="{Binding}"
                    ItemTemplate="{StaticResource ClosableTabItemTemplate}"
                    Margin="5">
        </TabControl>
    </DataTemplate>

Was ich brauche ist jetzt praktisch innerhalb des vorhandenen HeaderedContentControls noch mal ein HeaderedContentControl, welches als ContentTemplate ja wieder WorkspacesTemplate hernehmen könnte und dann nicht an die Collection von WorkspaceViewModels mit dem Namen Workspaces bindet sondern an eine ander Collection von WorkspaceViewModels die zum Beispiel WorkspaceGroups oder so heißt. Das Problem ist nur, dass das HeaderedContentControl keinen weiteren Inhalt aufnehmen kann, da sonst die Content Eigenschaft mehrfach vergeben wurde.

Wie müsste ich das Control jetzt anpassen, damit es noch ein weiteres ContentControl aufnehmen kann?

20.03.2014 - 18:11 Uhr

Hallo Zusammen,

sicherlich kennen viele von euch den folgenden Artikel zum MVVM in WPF WPF-Anwendungen mit dem Model-View-ViewModel-Entwurfsmuster und die dort dargestellte Demoanwendung.

Ich versuche gerade dieses Demo für mich zu erweitern (siehe angehängtes Bild), da das Demo "nur eine Ebene hat".

Wie Ihr sehen könnt habe ich Links die Commands jetzt schon erfolgreich in zwei Kategorien aufteilen können. Diese Struktur möchte ich jetzt natürlich auch in den Arbeitsbereichen widerspiegeln. Das heißt, dass ich hier zwei ineinander verschachtelte TabControls brauche. Wenn ich zum Beispiel auf den Button Lagerbestandsübersicht klicke, dann sollte unter Arbeitsbereiche ein Tab mit dem Titel Lagerverwaltung aufgehen und innerhalb dieses Tabs dann eben der Tab mit der Lagerbestandsübersicht.

Wie würdet ihr diese Übergeordnete TabControl hier einbauen?
Die Anwendung ist nach dem Beispiel zu oberen Link programmiert und ich habe im Moment einfach keine Idee, wie ich dieses TabControl da noch dazwischen bekomme.

Viele Grüße
Torsten

12.03.2014 - 20:26 Uhr

Hallo Zusammen,

ich bin gerade ein Beispiel in nem Buch über WPF 4.5 zum Thema SharedSizeGroup durchgegangen.

Da kann man rechts nen Bereich ein/ausblenden und pinnen und unpinnen.
Das ganze funktioniert echt super, aber ich würde das ganze gerne links haben
und habe keine Ahnung wie ich es gedreht kriege.

Hier erst mal ein bisschen Code:


<Grid IsSharedSizeScope="True">

<Grid x:Name="Layer0Grid"
                          MouseEnter="HandleLayer0MouseEnter">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>

<Border BorderBrush="#FF3F3F46"
             BorderThickness="1"> 
                            <TextBlock Margin="10" 
                                             Text="Arbeitsbereich"/>
</Border>

<Grid x:Name="Layer1Grid"
                          Visibility="Collapsed">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="5"/>
                            <ColumnDefinition SharedSizeGroup="PinSpalte" 
                                              Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        
                        <Border Grid.Column="2"
                                BorderBrush="#FF3F3F46"
                                BorderThickness="1"
                                Margin="0 10 5 5">
                            
                            <DockPanel Background="#FF252526">
                                
                                <Grid DockPanel.Dock="Top" Background="#FF2D2D30">
                                        <TextBlock HorizontalAlignment="Left"
                                                   Text="Aktionsbereich"
                                                   Foreground="White"
                                                   Margin="10 2 10 2"/>
                                        <ToggleButton x:Name="btnPinIt"
                                                      Margin="0 0 5 0"
                                                      HorizontalAlignment="Right"
                                                      Height="16"
                                                      Width="16"
                                                      Checked="HandlePinning"
                                                      Unchecked="HandleUnpinning"
                                                      IsChecked="False">
                                        
                                        </ToggleButton>
                                </Grid>
                                
                                <TextBlock Margin="10 5 10 5"
                                           Foreground="#FF999999"
                                           FontStyle="Italic">
                                   Blablablubb . . .</TextBlock>
                                                                        
                                
                            </DockPanel>
                            
                        </Border>
                        
                        <GridSplitter Grid.Column="1" 
                                      HorizontalAlignment="Left"
                                      Width="5" 
                                      Background="#FF2D2D30" 
                                      ResizeBehavior="PreviousAndNext"/>
                            
                    </Grid>

</Grid>

</Grid>

In der CodeBehind wird jetzt über die Handles ne ColumnDefinition dem Layer0Grid hinzugefügt, oder eben wieder gelöscht je nachdem, ob der PinIt Button eben gecheckt oder nicht gecheckt ist.

Siehe C# Code:


public partial class MainWindow : Window
{
    private ColumnDefinition dummySpalteFuerLayer0Grid;
    
    public MainWindow()
        {
            InitializeComponent();

            dummySpalteFuerLayer0Grid = new ColumnDefinition();
            dummySpalteFuerLayer0Grid.SharedSizeGroup = "PinSpalte";
        }
    
    private void HandlePinning(object sender, RoutedEventArgs e)
        {
            Layer0Grid.ColumnDefinitions.Add(dummySpalteFuerLayer0Grid);
        }

        private void HandleUnpinning(object sender, RoutedEventArgs e)
        {
             Layer0Grid.ColumnDefinitions.Remove(dummySpalteFuerLayer0Grid);
        }

}

Mein Problem ist, dass ich keine Ahnung habe, wie ich das ganze jetzt auf der linken Seite
umsetzen soll, denn die hinzugefügte ColumnDefinition ist ja automatisch immer rechts oder?

Kann mir einer von euch vielleicht nen kleinen "Schupser" geben?

Danke euch schon mal!
Viele Grüße Torsten

20.02.2014 - 15:22 Uhr

So Leute ich glaub ich hab jetzt genau den richtigen Weg gefunden, der mir von A-Z das Zusammenspiel der nötigen Komponenten erklärt.

Der Folgende Link führt zu einer Datenbankanwendung und Dokumentation einer fiktiven Fluggesellschaft, in der Schritt für Schritt eine Schicht nach der anderen aufgebaut wird.

WorldWideWings

Wenn ich dieses Beispiel durchgearbeitet und verstanden habe, sollte ich im Großen und Ganzen in der Lage sein, eine einigermaßen vernünftige Datenbankanwendung erstellen zu können.

Danke euch allen noch mal für eure Inputs, ohne die meine Suche wahrscheinlich ins Nirwana geführt hätte! 😃

20.02.2014 - 13:57 Uhr

wie etwas weiter oben empfohlen, habe ich mir das hier: http://msdn.microsoft.com/de-de/magazine/dd419663.aspx
jetzt mal "reingezogen". Das Grundprinzip wie die Dinge zusammen spielen, kann man gut herauslesen.

ABER: Es gibt ein Kernthema was nirgendwo veranschaulicht wird und das auch nicht in dem verlinkten Artikel.

In allen Beschreibungen die ich bis jetzt gefunden habe, werden immer nur Daten aus einer Datenquelle in das ViewModel geholt, die View bindet auf das ViewModel und man kann die Daten im ViewModel ändern, oder neue Objekte hinzufügen.

Für mich als Anfänger wäre aber hier erstmal der wichtigste Teil, wenn jemand beschreibt, wie ich die im ViewModel veränderten, oder hinzugefügten Daten am Ende in die Datenbank schreibe. Nirgendwo wird das aufgeführt.
Auch nicht in der Demoanwendung, die man sich zu dem verlinkten Artikel herunterladen kann.

Die Datenbasis für die Anwendung ist ein XML File, aber wenn man nun einen
neuen Customer hinzufügt und speichert, dann kommt der "nur" im ViewModel an. Schließt man die Anwendung und öffnet sie wieder, ist der neue Customer weg. Der Speichervorgang endet also im ViewModel und nicht in der Datenquelle, obwohl er in der Realität eben genau dort enden soll.

Hat jemand hier evtl. einen Link zu einem solchen Beispiel in dem der "Datenkreislauf" komplett geschlossen ist?

20.02.2014 - 11:43 Uhr

Ha! 😃 Besten Dank für die Motivation! Das kann man in solchen Momenten wirklich ab und an mal gebrauchen.

20.02.2014 - 11:33 Uhr

Hallo Abt,

Danke für deine Antwort.

Ich hatte auch nicht behauptet, dass ich mich komplett mit MVVM beschäftigt habe, sondern wie du weiter oben sehen kannst, habe ich in einem Buch ein kleines einfaches Beispiel zum MVVM angeschaut.

Als ich den Beitrag hier eröffnet hatte war ich gerade beim Thema EDM und
stieß dann eben auf das Thema/Problem, dass meine Benutzeroberfläche die
Änderungen nicht mitbekommt. Ich hätte nicht gedacht, dass die Antwort oder auch Lösung für das Problem ein für mich neues und sehr komplexes Thema sein wird.

So wie ich das hier sehe, seid ihr alle ziemlich gute Profis und genau so antwortet ihr eben auch.

Ich bringe mir das Programmieren selber über Bücher bei und das ist ziemlich zäh muss ich gestehen. Ich fürchte um das Problem hier lösen zu können muss ich erst mal noch weiter die Nase in die Bücher stecken.

Trotzdem Danke an euch alle!

20.02.2014 - 10:56 Uhr

So . . . das MVVM Beispiel hat mir leider nicht viel gebracht, weil es
da nur um die Navigation in den Datensätzen ging.

Mein Problem ist aber die Datenmanipulation und da fehlt mir noch ein kleines Stück vom Grundverständnis.

Ich will ja mit dem EDM arbeiten, weil der ObjectContext und der ObjectStateManager so tolle Dienste leisten, was die Überwachung
angeht und auch das LazyLoading ist bei riesigen Mengen ja richtig gut.

Wichtig ist hier eben auch, dass wenn 2 Benutzer am selben Datensatz fummeln und einer als erstes speichert, dass ich dann eben auch beim zweiten die OptimisticConcurrencyException kriege und alles abhandeln kann.

Beim MVVM hol ich die Daten doch aber von dort weg, in dem ich mir wie
oben vorgeschlagen zum Beispiel eine Liste ziehe.

Wenn ich dann Daten ändern, oder hinzufügen will, dann mach ich das doch dann wahrscheinlich in der Liste, oder?

Falls ja, hat die Liste dann doch keinen Bezug zum ObjektContext, oder?

In meinem Fall möchte ich die Aktualisierung ja auch richtig im DataGrid (oder wo auch immer) haben.
Wenn ich einen neuen Hersteller anlege, dann brauch ich ja auch die ihm vergebene ID im DataGrid, die ja aus der Datenbank kommen muss.

Ich finde in meinen Büchern aber auch leider kein Beispiel, in dem das mal an so einer ganz einfachen Sache wie hier durchgespielt wird.