Czy metoda AddLog koliduje z zapisem do pliku i usuwaniem rekordów w metodzie obsługiwanej przez timer?

Czy metoda AddLog koliduje z zapisem do pliku i usuwaniem rekordów w metodzie obsługiwanej przez timer?
1

Witam serdecznie,

napisałem sobie taką banalnie prostą klasę, która loguje mi pewne zdarzenia przychodzące z różnych miejsc w programie, poprzez wywołanie metody AddLog

Klasa zawiera:

  • konstruktor, który wywołuję metodę utworzenia tablicy i konfiguruje timer, który odpala się co 2 sekundy.
  • metodę dodania wpisu do tablicy AddLog
  • metodę wywołania timera, która najpierw robi zapis rekordów do pliku, a następnie usuwa te pliki

I wszystko generalnie działa, ale od czasu do czasu pojawiają sie wpisy o błędach z metody AddLog jak poniżej:

Kopiuj
91 System.Data Object reference not set to an instance of an object
5 System.Data Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection
5 System.Data DataTable internal index is corrupted

I nie wiem czy tutaj zachodzi jakaś kolizja metody, która usuwa rekordy a w tym samym momencie dokonywany jest zapis do tablicy?

Kopiuj
Imports System.IO

Public Class Log

    Public FileWriter As StreamWriter
    Private LogTablica As DataTable = New DataTable
    Private SAVELOGTMR As System.Timers.Timer

    Sub New()

        Try
            CreateLogTable()

            SAVELOGTMR = New System.Timers.Timer
        SAVELOGTMR.Interval = 2500
        SAVELOGTMR.Stop()
        AddHandler SAVELOGTMR.Elapsed, AddressOf SAVELOGTMR_Tick

        SAVELOGTMR.Start()

        Catch ex As Exception
        End Try

    End Sub

    Private Sub SAVELOGTMR_Tick(ByVal source As Object, ByVal e As System.Timers.ElapsedEventArgs)

        Try

            SAVELOGTMR.Stop()

            Dim ilosc_rekordow As Integer = 0
            Dim x As Integer = 0
            Dim i As Integer = 0

            ilosc_rekordow = LogTablica.Rows.Count

            FileWriter = New StreamWriter(".\Log.log", True)

            If LogTablica.Rows.Count() <> 0 Then

                'Zrzucam do pliku
                For i = 0 To ilosc_rekordow - 1
                    FileWriter.WriteLine(CStr(LogTablica.Rows(i).Item(1)) & " " & CStr(LogTablica.Rows(i).Item(2)))
                Next

                FileWriter.Close()

                'Usuwam rekordy zapisane
                For x = 0 To ilosc_rekordow - 1
                    LogTablica.Rows.RemoveAt(0)
                    LogTablica.AcceptChanges()
                Next

            End If

            SAVELOGTMR.Start()

        Catch ex As Exception
        Finally
            SAVELOGTMR.Start()
        End Try

    End Sub

    Public Sub AddLog(ByVal logitem As String)

        Try
            Dim Row As DataRow

            Row = LogTablica.NewRow()
            Row("DateTime") = Format(Now, "yyyy-MM-dd HH:mm:ss.fff")
            Row("LogItem") = logitem
            LogTablica.Rows.Add(Row)

            LogTablica.AcceptChanges()

        Catch ex As Exception
        End Try

    End Sub

    Private Sub CreateLogTable()

        Dim DateTime As DataColumn = New DataColumn("DateTime", GetType(String))
        Dim LogItem As DataColumn = New DataColumn("LogItem", GetType(String))

        Try

            LogTablica.Columns.Add(DateTime)
            LogTablica.Columns.Add(LogItem)

        Catch ex As Exception
            Call Error_sub_main(System.Reflection.MethodBase.GetCurrentMethod.Name.ToString)
        End Try

    End Sub

End Class
1

Hej

a w jakim celu w zdarzeniu Private Sub SAVELOGTMR_Tick(ByVal source As Object, ByVal e As System.Timers.ElapsedEventArgs) masz

Kopiuj
SAVELOGTMR.Stop() a potem  SAVELOGTMR.Start()

tak samo to

Kopiuj
Finally
    SAVELOGTMR.Start()
End Try

timer bedzie startował co 2500 i to wystarczy.. zapis do pliku zostaw tylko w try'u. Na czas zapisu nie ma potrzeba zatrzymuwania timera

1
sight napisał(a):

Hej

a w jakim celu w zdarzeniu Private Sub SAVELOGTMR_Tick(ByVal source As Object, ByVal e As System.Timers.ElapsedEventArgs) masz

SAVELOGTMR.Stop() a potem SAVELOGTMR.Start()

tak samo to

Finally
SAVELOGTMR.Start()
End Try

timer bedzie startował co 2500 i to wystarczy.. zapis do pliku zostaw tylko w try'u. Na czas zapisu nie ma potrzeba zatrzymuwania timera

Cześć,

dzięki za zainteresowanie tematem.
Zatrzymuję timer bo boję się, że jeśli będzie więcej rekordów to zapis do pliku będzie trwał dłużej niż wywołanie timera i w międzyczasie wystąpi kolejne, które zapewne zwróci i tak błąd, jeśli plik będzie nadal otwarty do zapisu z poprzedniego wywołania. Nie wiem, może to błędne myślenie.
A w Finally daję start z tego powodu, że gdyby coś nie poszło na etapie zapisu do pliku i zostanie pominięta linia startu w normalnym wykonaniu kodu, to timer zostanie wystartowany w trybie obsługi błędu.

Popraw mnie jeśli źle rozumuję.

1
Almatea napisał(a):

dzięki za zainteresowanie tematem.
Zatrzymuję timer bo boję się, że jeśli będzie więcej rekordów to zapis do pliku będzie trwał dłużej niż wywołanie timera i w międzyczasie wystąpi kolejne, które zapewne zwróci i tak błąd, jeśli plik będzie nadal otwarty do zapisu z poprzedniego wywołania. Nie wiem, może to błędne myślenie.
A w Finally daję start z tego powodu, że gdyby coś nie poszło na etapie zapisu do pliku i zostanie pominięta linia startu w normalnym wykonaniu kodu, to timer zostanie wystartowany w trybie obsługi błędu.

Nie ma co się bać - sprawdź to.

Przy zapisie do pliku dopisz sobie Threading.Thread.Sleep(300) żeby zasymulować powolny zapis. Sprawdź co się stanie - jeśli dostaniesz błąd, to sobie go obsłuż w odpowiedni sposób. Jeśli nie, to nie ma problemu.

0

no dobra mozna podzialac z Threading.Thread.Sleep(300) to doby pomysl ale zastanawiam sie po co tyle klas w tym programie.. program zakładam ze co 2,5 sec ma zapisywac cos do pliku.. co to był to zrobił tylko w timerze i tyle..

0
sight napisał(a):

no dobra mozna podzialac z Threading.Thread.Sleep(300) to doby pomysl ale zastanawiam sie po co tyle klas w tym programie.. program zakładam ze co 2,5 sec ma zapisywac cos do pliku.. co to był to zrobił tylko w timerze i tyle..

To jest jedna klasa Log, która odpowiada za obsługę Logowania zdarzeń z danego urządzenia, który generuje je różne i niezależne od siebie.
Tych urządzeń jest więcej, więc klasa Log jest przypisana do każdego z urządzeń osobno. Oczywiście posiada również swój index jak i unikatową nazwę pliku. Tutaj dla uproszczenia wpisałem Error.log, a w rzeczywistości nazwa posiada jeszcze dopisek rozróżniający którego urządzenia log się tyczy. I tak jak pisałem, generalnie działa to bez problemu, tylko na 24 h pojawia się kilkanaście wpisów o błędach z komunikatami, które opisałem na początku.
Przetestuję ten threading.sleep(300).

0

System.Timers.Timer nie czeka na zakończenie zdarzenia obsługi. Rozwiązałeś to poprawnie zatrzymując timer na czas obsługi zdarzenia. Ale handler Elapsed może być wykonywany w osobnym wątku (https://stackoverflow.com/questions/7893773/do-system-timers-timer-run-in-independent-threads), co jest moim zdaniem przyczyną Twoich problemów. Na Twoich danych działa inny wątek (który woła AddLog), a klasa opakowująca dane ani dostęp do niej nie są thread-safe -> BUM.

Zamiast użycia dodatkowej, współdzielonej klasy, która zapewnia znacznie więcej funkcjonalności niż tylko opakowanie danych, użyj przy każdym zapisie nowej instancji dedykowanego modelu, albo zamknij w tym samym muteksie (np. przez lock) każdy zapis i odczyt, a najlepiej zrób i jedno, i drugie.

BTW DataTable ma metodę Clear.

0
ŁF napisał(a):

System.Timers.Timer nie czeka na zakończenie zdarzenia obsługi. Rozwiązałeś to poprawnie zatrzymując timer na czas obsługi zdarzenia. Ale handler Elapsed może być wykonywany w osobnym wątku (https://stackoverflow.com/questions/7893773/do-system-timers-timer-run-in-independent-threads), co jest moim zdaniem przyczyną Twoich problemów. Na Twoich danych działa inny wątek (który woła AddLog), a klasa opakowująca dane ani dostęp do niej nie są thread-safe -> BUM.

Zamiast użycia dodatkowej, współdzielonej klasy, która zapewnia znacznie więcej funkcjonalności niż tylko opakowanie danych, użyj przy każdym zapisie nowej instancji dedykowanego modelu, albo zamknij w tym samym muteksie (np. przez lock) każdy zapis i odczyt, a najlepiej zrób i jedno, i drugie.

BTW DataTable ma metodę Clear.

Dzięki serdeczne za odpowiedź i pomoc.
Czyli na wstępie sugerujesz wykorzystanie System.Threading.Timer zamiast System.Timers.Timer.

Ja jeszcze zrobiłem taki myk, że przeniosłem funkcjonalność zapisu do pliku do osobnej procedury, a w wywołaniu timera zrobiłem wywołanie tej procedury. I usuwanie rekordów również rozwiązałem w inny sposób. Zamiast RemoveAt() używam Delete. I to generalnie działa, do momentu aż się coś posypie. Wtedy mam kilkanaście wpisów (9 System.Data There is no row at position x) i znowu wszystko wraca do normy. Zastosowałem jeszcze (nie wiem czy słusznie Lock as New Object). Wygląda to tak jakby rzeczywiście zachodziła kolizja między AddLog a Write_log_to_file. Teraz kod wygląd tak:

Kopiuj
Imports System.IO
Imports System.Reflection.Emit
Imports System
Imports System.Windows.Forms

Public Class Log

    Public FileWrite As StreamWriter
    Private LogTablica As DataTable = New DataTable
    Private SAVELOGTMR As System.Timers.Timer

    Sub New()

        Try
            CreateLogTable()

            SAVELOGTMR = New System.Timers.Timer
            SAVELOGTMR.Interval = 2500
            SAVELOGTMR.Stop()
            AddHandler SAVELOGTMR.Elapsed, AddressOf SAVELOGTMR_Tick

            SAVELOGTMR.Start()

        Catch ex As Exception
        End Try

    End Sub

    Private Sub SAVELOGTMR_Tick(ByVal source As Object, ByVal e As System.Timers.ElapsedEventArgs)

        Try

            Dim ilr As Integer = 0

            ilr = LogTablica.Rows.Count

            If ilr = 0 Then
                Exit Sub
            Else
                Write_log_to_file()
            End If

        Catch ex As Exception

        Finally

        End Try

    End Sub

    Public Lock_Write_log_to_file As New Object
    Private Sub Write_log_to_file()

        SyncLock Lock_Write_log_to_file
            Try

                Dim ilr As Integer = 0
                Dim x As Integer = 0
                Dim i As Integer = 0

                ilr = LogTablica.Rows.Count

                FileWrite = New StreamWriter(".\Lognew.log", True)
                Dim senddata As String=""

                If ilr > 0 Then

                    For i = 0 To ilr - 1

                        senddata = (CStr(LogTablica.Rows(i).Item(0)) & " " & CStr(LogTablica.Rows(i).Item(1)) & " LogRows: " &  LogTablica.Rows.Count.ToString)
                        FileWrite.WriteLine(senddata)

                    Next

                    FileWrite.Close()

                    For x = ilr - 1 To 0 Step -1
                        Dim dr As DataRow = LogTablica.Rows(x)
                        dr.Delete()
                    Next

                End If

            Catch ex As Exception

            Finally
                FileWrite.Close()
            End Try

        End SyncLock

    End Sub

    Public Sub AddLog(ByVal logitem As String)

        Try
            Dim Row As DataRow

            Row = LogTablica.NewRow()
            Row("DateTime") = Format(Now, "yyyy-MM-dd HH:mm:ss.fff")
            Row("LogItem") = logitem
            LogTablica.Rows.Add(Row)

            LogTablica.AcceptChanges()

        Catch ex As Exception
        End Try

    End Sub

    Private Sub CreateLogTable()

        Dim DateTime As DataColumn = New DataColumn("DateTime", GetType(String))
        Dim LogItem As DataColumn = New DataColumn("LogItem", GetType(String))

        Try

            LogTablica.Columns.Add(DateTime)
            LogTablica.Columns.Add(LogItem)

        Catch ex As Exception
            'Call Error_sub_main(System.Reflection.MethodBase.GetCurrentMethod.Name.ToString)
        End Try

    End Sub

End Class

0
Almatea napisał(a):

Czyli na wstępie sugerujesz wykorzystanie System.Threading.Timer zamiast System.Timers.Timer.

Nie, nie robiłem tego, ale jak teraz przyjrzałem się mu uważniej, to IMHO System.Threading.Timer będzie zdecydowanie lepszy. Może gotowy kod lepiej Ci wytłumaczy, przy okazji zobaczysz, na czym polega SRP. Wybacz, że w C#, ale VB powoduje u mnie odruch wymiotny; myślę, że i tak się połapiesz co i jak:

Kopiuj
class LogItem
{
    public DateTime DateTime { get; } = DateTime.Now;
    public string Message { get; }
    
    public LogItem(string message) => Message = message ?? "";

    public string GetLogItem() => $"{DateTime} {Message}";
}

class LogItemList
{
    public List<LogItem> Items { get; } = [];
    public bool HasAny() => Items.Any();
    public void Add(string message) => Items.Add(new LogItem(message));
}

class LogItemListPersistenceService
{
    private string path { get; }
    public LogItemListPersistenceService(string path)
        => this.path = path;

    public void Save(LogItemList logItemList)
        => File.AppendAllLines(path, logItemList.Items.Select(i => i.GetLogItem()));
}

class Logger
{
    private LogItemList logItemList = new();
    private readonly System.Timers.Timer timer = new (new TimeSpan(0, 0, 0, 0, 2500));
    private static readonly object locker = new();
    private readonly LogItemListPersistenceService persistenceService;
    private bool isSaving = false;

    public Logger(LogItemListPersistenceService persistenceService)
    {
        this.persistenceService = persistenceService;
        timer.Enabled = true;
        timer.Elapsed += OnTimerTick;
    }

    private void OnTimerTick(object? sender, System.Timers.ElapsedEventArgs e)
    {
        if (!isSaving && logItemList.HasAny())
        {
            var lastLogItemList = logItemList;
            lock (locker)
            {
                if (isSaving) return;
                isSaving = true;
                logItemList = new LogItemList();
            }
            try
            {
                persistenceService.Save(lastLogItemList);
            }
            finally
            {
                isSaving = false;
            }
            lastLogItemList.Items.Clear();
        }
    }

    public void Log(string message)
    {
        lock (locker)
        {
            logItemList.Add(message);
        }
    }
}

Używasz np. tak:

Kopiuj
      var logger = new Logger(new LogItemListPersistenceService(@"C:\svn\log.txt"));
      for (int i = 0; i < 100000; i++) { logger.Log(i.ToString()); Thread.Sleep(Random.Shared.Next() % 2); }

Zauważ, że ten sam lock jest użyty na zapisie, jak i odczycie z logItemList, ale jest ustawiany tylko na prostą operację przypisania, a nie na cały czas zapisu. Ponadto dodałem sprawdzanie, czy nie odbywa się właśnie równoległy zapis, jeśli tak, to bieżący jest przekładany na następne tyknięcie zegara. Przy ekstremalnie dużej ilości danych może to doprowadzić do przepełnienia bufora na logi, ale jeśli zapis odbywa się w osobnym wątku tak jak tutaj, to obejście tego wymaga napisania znacznie więcej kodu.

0

Nie, nie robiłem tego, ale jak teraz przyjrzałem się mu uważniej, to IMHO System.Threading.Timer będzie zdecydowanie lepszy. Może gotowy kod lepiej Ci wytłumaczy, przy okazji zobaczysz, na czym polega SRP. Wybacz, że w C#, ale VB powoduje u mnie odruch wymiotny; myślę, że i tak się połapiesz co i jak:

Używasz np. tak:

Kopiuj
      var logger = new Logger(new LogItemListPersistenceService(@"C:\svn\log.txt"));
      for (int i = 0; i < 100000; i++) { logger.Log(i.ToString()); Thread.Sleep(Random.Shared.Next() % 2); }

Zauważ, że ten sam lock jest użyty na zapisie, jak i odczycie z logItemList, ale jest ustawiany tylko na prostą operację przypisania, a nie na cały czas zapisu. Ponadto dodałem sprawdzanie, czy nie odbywa się właśnie równoległy zapis, jeśli tak, to bieżący jest przekładany na następne tyknięcie zegara. Przy ekstremalnie dużej ilości danych może to doprowadzić do przepełnienia bufora na logi, ale jeśli zapis odbywa się w osobnym wątku tak jak tutaj, to obejście tego wymaga napisania znacznie więcej kodu.

Dzięki serdeczne za kod. Na pewno go przeanalizuję. Tu jest widzę wykorzystana lista a nie tablica danych. Nie pomyślałem o tym. Może rzeczywiście będzie lepszym rozwiązaniem. Do tego chyba jeszcze wszystko napisane zgodnie z pierwszą zasadą Solid. Mam rację czy się mylę? Szacun za kod, naprawdę. Przyznam się, że przechodzę małymi krokami do C#.
Jeśli mogę zapytać przy okazji o coś jeszcze. W VB.Net jest coś takiego, że to co w klauzli Module zdefiniowane jest z akcesorem Public, jest dostępne globalnie dla całego projektu. Wiem, że w C# nie ma czegoś takiego, ale można to chyba rozwiązać przy pomocy klasy statycznej? Ja nie wiem na ile to ma sens, ale teoretyzując - jak można by rozwiązać inaczej taki kod? Klasa Utworz_tablice definuje tablice, Klasa Odczytaj_Z_Tablicy zawiera jedną funkcję, która zwraca wartość rekordu poprzez indeks, a Klasa Zapisz_Do_Tablicy dodaje nowy rekord z wartościa z parametru. I wszystkie te klasy operują na tablicy zdefiniowanej w klasie statycznej. Czy np. zamiast tablicy jakiejś liście etc etc Osobiście nie podoba mi sie takie rozwiązanie i strzelam, że nie jest dobre, a wręcz tragiczne. Ale nie wiem jak można by to inaczej rowiązać. Zastanawiałem się nad dziedziczeniem, ale zapytam lepiej eksperta. Jesli mogę prosić o modyfikację tego kodu, to bedzie to dla nnie cenna wskazówka, za którą z góry dziękuję.

Kopiuj
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualBasic;

static class Module1
{
    public static DataTable tablica;
}

public class Utworz_tablice
{
    public Utworz_tablice()
    {
        DataColumn Item = new DataColumn("Item", typeof(string));
        Module1.tablica.Columns.Add(Item);
    }
}

public class Odczytaj_Z_Tablicy
{
    public string odczytaj(int index)
    {
        return Module1.tablica.Rows(index).Item(0);
    }
}

public class Zapisz_Do_Tablicy
{
    public void zapisz(string item)
    {
        DataRow Row;

        Row = Module1.tablica.NewRow();
        Row("Item") = item;
    }
}

0

Nie wiem, od czego zacząć, bo trochę rzeczy zrobiłeś nie tak, więc może standardowo przychrzanię się do nazw.

  1. Nazywaj rzeczy po angielsku.
  2. Używaj składni powiązanej z językiem. Dla C# jest to CamelCase (aka PascalCase) dla metod, klas i publicznych pól, właściwości i stałych, a dla prywatnych pól i zmiennych lokalnych - javaCamelCase. Nie używaj podkreśleń (są od tego wyjątki, ale nie występują w Twoim kodzie).

Teraz bardziej na temat.
1 Używanie statycznej zmiennej na trzymanie danych to zwykle kiepski pomysł (singleton to antywzorzec). Używanie statycznego pola w dedykowanej klasie trzymającej tylko to pole i nie definiującej żadnej funkcjonalności, to bardzo zły pomysł. Tworzenie klasy per wycinek funkcjonalności to absolutne no go. Z jednej strony, klasa powinna udostępniać tylko metody, które są niezbędne to zaimplementowania danej funkcjonalności. Z drugiej - klasa powinna zawierać kompletną funkcjonalność (często celem zaspokojenia SRP elementy tej funkcjonalności są zdefiniowane w innych klasach użytych przez "główną" klasę). Nie czyni się widocznymi poza klasą wewnętrznych elementów klasy, bo obiekt klasy musi być integralny - mieć możliwość pilnowania swojego stanu i zagwarantowania, że zewnętrzny kod nie zaburzy tego stanu. W przeciwnym wypadku pojawiają się trudne do namierzenia i naprawienia błędy -> trzeba więcej czasu na utrzymanie kodu -> staje się to drogie.
Przykład:
Klasa MyList implementuje IMyList i zawiera prywatne pole z listą i niezbędne metody do dodania/pobrania/usunięcia/zmiany elementów (niekoniecznie wszystkie). Tak zaprojektowana klasa sama sobie inicjuje pole z listą np. w konstruktorze, więc odpadnie możliwość odwołania się do nulla, a jej metody nie działają na globalnym obiekcie. Jeśli będziesz potrzebować synchronicznego dostępu do takiej listy, to robisz sobie dekorator (np. MyThreadSafeListDecorator, albo wręcz MyThreadSafeList), który implementuje ten sam interfejs IMyList (co za pośrednictwem IoC pozwoli płynnie zmienić implementację), a wewnętrznie w synchroniczny sposób (np. wewnątrz lock) woła metody enkapsulowanej klasy MyList.
Pseudokod:

Kopiuj
interface IMyList { Add(string item); }
class MyList : IMyList
{
    private List<string> list = new();
    public Add(string item) => list.Add(item);
}
class MyThreadSafeListDecorator : IMyList
{
    private MyList list = new();
    private object locker = new();
    public Add(string item) => { lock(locker) { list.Add(item); } }
}

W tak napisanych klasach zagwarantowanie synchroniczności per instancja listy jest trywialne. Ponadto zobacz, gdzie odbywa się inicjalizacja pól list - robi się ona inline, a więc tak, jakby zrobił to konstruktor. To gwarantuje, że stan obiektu w czasie jego działania będzie prawidłowy i niemożliwy do zaburzenia z zewnątrz bez użycia specjalnych narzędzi (vide refleksja). Możesz wtedy też zagwarantować zainicjowanie stanu obiektu tylko prawidłowymi wartościami, jak w przykładzie poniżej, gdzie list jest inicjalizowane w konstruktorze:

Kopiuj
class MyThreadSafeListDecoratorWithExternalList : IMyList
{
    private IMyList list;
    private object locker = new();
    public MyThreadSafeListDecoratorWithExternalList(IMyList list)
    {
        this.list = list ?? throw new ArgumentNullException(nameof(list)); // if argument list is null throw exception
    }
    // ...
}

Tu masz zagwarantowane, że mimo iż list przychodzi z zewnątrz, to nie będzie nullem, czyli obiekt nie da się nieprawidłowo zainicjalizować.

2 Używaj klas najlepiej dopasowanych do tego, co potrzebujesz zrobić. Już Ci to pisałem, nie wiem czemu to zignorowałeś. Nie używaj DataTable z pierdyliardem metod, kiedy wystarczy Ci List. DataTable, które AFAIK jest komponentem UI, będzie o rzędy wielkości wolniejsze od List.

3 Row("Item") - odwołujesz się do kolumny, jednocześnie ustalając jej nazwę w innym miejscu. Za kilka miesięcy zmienisz jej nazwę w którymś miejscu i zapomnisz poprawić ją w każdym innym. Albo użyj stałej, żeby mieć jedną nazwę zdefiniowaną w jednym jedynym miejscu, albo - najlepiej - do trzymania danych użyj obiektu, który nie potrzebuje nazwy kolumny, ponieważ to jedyna kolumna, a jej nazwa nic Ci nie daje, nie wyświetlasz jej, nie ma innych kolumn, jest tylko ta jedna.

Więcej uwag jak będę mieć więcej czasu.

1
ŁF napisał(a):

2 Używaj klas najlepiej dopasowanych do tego, co potrzebujesz zrobić. Już Ci to pisałem, nie wiem czemu to zignorowałeś. Nie używaj DataTable z pierdyliardem metod, kiedy wystarczy Ci List. DataTable, które AFAIK jest komponentem UI, będzie o rzędy wielkości wolniejsze od List.

Nie zignorowałem. Przerobiłem właściwy program na 'List' zamiast 'DataTable' i chyba problem zniknął. Oczywiście w tym przykładzie C# została jeszcze tablica, ale to był tylko przykład.
Naprawdę wielkie DZIĘKUJĘ za poświęcony czas.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.