Προς το περιεχόμενο

Προτεινόμενες αναρτήσεις

Δημοσ.

   Αυτο που δεν καταλαβα, ειναι εφοσον τα περισσοτερα queries οπως ειπες παιρνουνε 1 seconds, τι λογικη εχει να τα ακυρωσεις; Εστω και 3,4 δευτερα το μεγιστο που ανεφερες, δεν ειναι λογικο να υπαρχουν πολλαπλα requests σε αυτο το διαστημα. Μεχρι να στειλεις το cancelation token, το request θα εχει τελειωσει.

 

   Και μια αλλη παρατηρηση δεν βλεπω λογο να προβαλεις 1500 σειρες ταυτοχρονα σε ενα datagrid. Σπαστα σε περισσοτερες απο μια σελιδες, η καθε μια να δειχνει χ αριθμο σειρων τον οποιο ο χρηστης καθοριζει. Για ακομη καλυτερη αποδoση οταν φορτωνεις μια, με background threads να ετοιμαζεις την επομενη και την προηγουμενη, αλλα αμα προβαλεις μερικες εκατονταδες σε καθε σελιδα, δεν ειναι απαραιτητο. Και με ενα απλο search box κανεις ενα γρηγορο search σε ολο το table. 

 

Ίσως δεν ήμουν ακριβώς σαφής, δεν είναι το Query που θέλω να ακυρώσω, είναι όλο το Thread που τραβάει τα δεδομένα και τα εμφανίζει στο grid. Το βαρύ κομμάτι, είναι το πέρασμα στο grid. Μιλάμε για 10άδες στήλες, με Calcullated fields, με conditional formatting και τρέχα γύρευε. Είναι βαριά διαδικασία, και θέλω να γίνεται ΜΙΑ φορά και ασύγχρονα!

 

 

Τα ακυεωνεις για να μην έχεις bottleneck. Το αν ακυρωθεί είναι αδιάφορο, το ζητούμενο είναι να μην έχεις δύο και queries.

 

 

Χρησιμοποιούμε telerik open access και δεν βρίσκω τρόπο να abort το Query.. Επίσης λόγω του ότι πρόκειται για σχετικά ελαφρύ query (κι ας έχει μεγάλο αποτέλεσμα) θεωρώ ότι αυτή την στιγμή είναι δευτερεύον πρόβλημα..

Λοιπόν, οιδού ο (ελαφρώς παραποιημένος) κώδικας...

static CancellationTokenSource _ctsRequeryOrders = new CancellationTokenSource();
        static TaskFactory<int> _tsfRequeryOrders = new TaskFactory<int>();
        static Task<int> _tskRequeryOrders;
        static async void RequeryOrders(vmOrderview vm)
        {
            if (_ctsRequeryOrders != null)
                if (_ctsRequeryOrders.Token != null)
                    if (_ctsRequeryOrders.Token.CanBeCanceled && !_ctsRequeryOrders.IsCancellationRequested)
                    {
                        _ctsRequeryOrders.Cancel();
                        _ctsRequeryOrders.Dispose();
                        _ctsRequeryOrders = new CancellationTokenSource();

                        Console.WriteLine("Canceled requeryOrders " + DateTime.Now.ToString("HH:mm:ss.fff"));
                    }

            _tskRequeryOrders = _tsfRequeryOrders.StartNew((x) =>
            {
                //Εμφανίζει το Loading bar στο grid
                vm.VIEW_ORDERS.Dispatcher.Invoke((ThreadStart)delegate
                {
                    vm.IsWorking = true;
                });

                    lock (vm.LIST_OF_ORDERS)
                    {
                            var QueryOfOrders = (from o in vm.Context.ORDERs
                                                 where o.OrderDate >= vm.FromDate && o.OrderDate <= vm.ToDate
                                                 select o).ToList();

                            vm.LIST_OF_ORDERS.Clear();
                            // Στιγμή εκτέλεσης του query. Το πρώτο αίμα
                            vm.LIST_OF_ORDERS.AddRange(QueryOfOrders);
  
                      }
                

                vm.VIEW_ORDERS.Dispatcher.Invoke((ThreadStart)delegate
                {
                    try
                    {
                        //εδώ γαμιέται ο Δίας
                        vm.VIEW_ORDERS.View.Refresh();
                    }
                    catch (Exception)
                    { }
                    vm.IsWorking = false;
                });
                
                return 0;
            }, _ctsRequeryOrders.Token);
                        
            await _tskRequeryOrders;
        }

Σημείωση:

1. vmOrderView είναι το viewmodel (δουλεύουμε WPF)

2. To VIEW_ORDERS είναι το ItemSource στο Grid μου, την στιγμή που κάνουμε Refresh στο VIEW_ORDERS.View παιρνιούνται τα δεδομένα στο grid (και αν είναι πολλά κολλάει το σύμπαν)

Δημοσ.

Οκ εσυ κάνεις cancel το τοκεν αλλα δεν εχεις πουθενα κωδικα που να κανει handle το cancellation 

 

Link.png Site: Γενικαγια cancellation

 

Link.png Site: Ποιοειδικα για cancellation σε Linq query

 

Εσυ επι της ουσιας θελεις να κανεις 2 πραγματα α) ενα SQL select query το οποιο ειναι IO bound και β) ενα grid view update το οποιο απ'οτι λες ειναι CPU bound. Το ΙΟ bound task εχει νοημα να γινει async, το CPU bound task δεν πρεπει να ειναι async κατ αναγκη, απλα πρεπει να χτισεις μια λογικη ωστε να μπορεις να το ακυρωνεις.

Δημοσ.

Δεν εχει νοημα να μιλαμε για canceling σε ενα thread 2,3 δευτερολεπτων. 

 

Και πως ειναι δυνατον να μην ελεγχεις μετα το query σου και πριν φορτωσεις τα δεδομενα στο data grid, αμα εχει προηγηθει αλλο request; 

 

Τι φαση το 

 

_tskRequeryOrders = _tsfRequeryOrders.StartNew((x) =>

{
//Εμφανίζει το Loading bar στο grid
vm.VIEW_ORDERS.Dispatcher.Invoke((ThreadStart)delegate
{
vm.IsWorking = true;
});

 
αντι για να κανεις 
vm.IsWorking = true;
και μετα  _tskRequeryOrders = _tsfRequeryOrders.StartNew((x) =>
...
 
 
Επιπλεον το new thread πρεπει να τελειωνει οταν ολοκληρωθει το query σου. Σε αυτο το σημειο θα εχεις ενα datatable, list, whatever που θα το βαλεις στο datagrid, αντι να εισαι ακομη στο background thread και να περιμενεις να δειξεις τα δεδομενα στο main thread και μετα να λειξει. Στην διαρκεια μαλιστα αυτην, εχεις καταφερει να εχεις κολλημενα και το main thread, και το background thread.
 
 
Και δεν υπαρχει ποτε λογος, ειδικα αν μιλαμε οτι δουλευεις σε main thread να εχεις static methods. Ειναι απλα code smell και καθε φορα που προσπαθεις να κανεις ενα νεο query, περιμενει το παλιο να φορτωσει τα δεδομενα!
Προς θεου μην περνας ποτε το view model σε static method αντι να εχεις την method στο view model. Αμα ο σκοπος σου ειναι να μην εχεις code duplication, περνα το RequeryOrders μαζι με μερικα αλλα helper functions που πιθανοτατα θα εχεις σαν constuctor injection στο view model. 
 
Αμα απλα δεν ητανε στατικ, και ειχες το request μονο σε new thread, τοτε θα μπορουσες να εχεις και δυο παραλληλα requests, και οταν μαλιστα τελειωνε το πρωτα, με ενα απλο check θα μπορουσες να δεις οτι τρεχει ηδη ενα αλλο query οποτε και απλα θα αγνοουσες τα αποτελεσμα του, και θα φορτωνες μονο τα αποτελεσματα του δευτερου. Ο ελεγχος δε ειναι απλουστατος, οταν κανεις new thread σωζεις το thread Id. Αμα μετα την ολοκληρωση του query η μεταβλητη σου εχει αλλο κωδικο, τοτε αγνοεις τα αποτελεσματα.
  • Like 1
Δημοσ.

Οκ εσυ κάνεις cancel το τοκεν αλλα δεν εχεις πουθενα κωδικα που να κανει handle το cancellation 

 

Link.png Site: Γενικαγια cancellation

 

Link.png Site: Ποιοειδικα για cancellation σε Linq query

 

Εσυ επι της ουσιας θελεις να κανεις 2 πραγματα α) ενα SQL select query το οποιο ειναι IO bound και β) ενα grid view update το οποιο απ'οτι λες ειναι CPU bound. Το ΙΟ bound task εχει νοημα να γινει async, το CPU bound task δεν πρεπει να ειναι async κατ αναγκη, απλα πρεπει να χτισεις μια λογικη ωστε να μπορεις να το ακυρωνεις.

 

Έχεις δίκιο, το διάβασα λίγο μετά το post και πλέον έχω σχεδόν σε κάθε γραμμή αυτό:

 
                            if (_ctsRequeryOrders.IsCancellationRequested) return 0;
 
Αυτό με το IO bound μπορείς να το αναλύσεις λίγο; Όπως προείπα, παίζουμε με Telerik Open Access και εκεί δεν γίνεται cancelation στα queries. Έχεις κάποια ιδέα;

 

 

Δεν εχει νοημα να μιλαμε για canceling σε ενα thread 2,3 δευτερολεπτων. 

 

Και πως ειναι δυνατον να μην ελεγχεις μετα το query σου και πριν φορτωσεις τα δεδομενα στο data grid, αμα εχει προηγηθει αλλο request; 

Έχεις δίκιο, πως όμως να το ελέγξω; Επιπλέον δεν με ενδιαφέρει αν έχει προηγηθεί, αλλά αν έχει ακολουθήσει. Με ενδιαφέρει το αποτέλεσμα ΜΟΝΟ του τελευταίου..

Τι φαση το 

 

_tskRequeryOrders = _tsfRequeryOrders.StartNew((x) =>

{

//Εμφανίζει το Loading bar στο grid

vm.VIEW_ORDERS.Dispatcher.Invoke((ThreadStart)delegate

{

vm.IsWorking = true;

});

 
αντι για να κανεις 
vm.IsWorking = true;
και μετα  _tskRequeryOrders = _tsfRequeryOrders.StartNew((x) =>
...
Δεν είχα κάποιο ιδιαίτερο λόγο που το έβαλα μέσα, απλώς ήθελα να είναι όλα μέσα στο "cancelable" thread.
 
Επιπλεον το new thread πρεπει να τελειωνει οταν ολοκληρωθει το query σου. Σε αυτο το σημειο θα εχεις ενα datatable, list, whatever που θα το βαλεις στο datagrid, αντι να εισαι ακομη στο background thread και να περιμενεις να δειξεις τα δεδομενα στο main thread και μετα να λειξει. Στην διαρκεια μαλιστα αυτην, εχεις καταφερει να εχεις κολλημενα και το main thread, και το background thread.
Bingo!!! Ακριβώς αυτό είναι το πρόβλημα μου! Θέλω ένα thread να τραβάει δεδομένα (και το συγκεκριμένο να μπορεί να επικαλύπτεται από νέο ανά πάσα στιγμή) και όταν αυτό ολοκληρωθεί να πετάω τα δεδομένα στο grid! Αυτό δεν μπορώ να κάνω! Ξέρεις κάποιον τρόπο;
 
Και δεν υπαρχει ποτε λογος, ειδικα αν μιλαμε οτι δουλευεις σε main thread να εχεις static methods. Ειναι απλα code smell και καθε φορα που προσπαθεις να κανεις ενα νεο query, περιμενει το παλιο να φορτωσει τα δεδομενα!
 
Προς θεου μην περνας ποτε το view model σε static method αντι να εχεις την method στο view model. Αμα ο σκοπος σου ειναι να μην εχεις code duplication, περνα το RequeryOrders μαζι με μερικα αλλα helper functions που πιθανοτατα θα εχεις σαν constuctor injection στο view model. 
 
Αμα απλα δεν ητανε στατικ, και ειχες το request μονο σε new thread, τοτε θα μπορουσες να εχεις και δυο παραλληλα requests, και οταν μαλιστα τελειωνε το πρωτα, με ενα απλο check θα μπορουσες να δεις οτι τρεχει ηδη ενα αλλο query οποτε και απλα θα αγνοουσες τα αποτελεσμα του, και θα φορτωνες μονο τα αποτελεσματα του δευτερου. Ο ελεγχος δε ειναι απλουστατος, οταν κανεις new thread σωζεις το thread Id. Αμα μετα την ολοκληρωση του query η μεταβλητη σου εχει αλλο κωδικο, τοτε αγνοεις τα αποτελεσματα.

 

Μαζί σου, δεν υπήρχε κανένας λόγος για static, απλά έμπλεξα τα μπούτια μου και δοκίμαζα ό,τι μου'ρχόταν...

Δημοσ.

 

Έχεις δίκιο, το διάβασα λίγο μετά το post και πλέον έχω σχεδόν σε κάθε γραμμή αυτό:

 
                            if (_ctsRequeryOrders.IsCancellationRequested) return 0;
 
Αυτό με το IO bound μπορείς να το αναλύσεις λίγο; Όπως προείπα, παίζουμε με Telerik Open Access και εκεί δεν γίνεται cancelation στα queries. Έχεις κάποια ιδέα;

 

Στο τελος εχω παραθεσει 

 

 Ο ελεγχος δε ειναι απλουστατος, οταν κανεις new thread το πρωτο πραγμα που κανεις ειναι να σωζεις το thread Id. Αμα μετα την ολοκληρωση του query η μεταβλητη σου εχει αλλο κωδικο, τοτε αγνοεις τα αποτελεσματα επειδη σημαινει οτι υπαρχει νεοτερο request.

Δημοσ.

Στο τελος εχω παραθεσει 

 

 Ο ελεγχος δε ειναι απλουστατος, οταν κανεις new thread το πρωτο πραγμα που κανεις ειναι να σωζεις το thread Id. Αμα μετα την ολοκληρωση του query η μεταβλητη σου εχει αλλο κωδικο, τοτε αγνοεις τα αποτελεσματα επειδη σημαινει οτι υπαρχει νεοτερο request.

 

 

Καλώς, το δοκιμάζω και επανέρχομαι

Δημοσ.

δες εδω

 

 

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace wpf_test
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        CancellationTokenSource token;
        public MainWindow()
        {
            InitializeComponent();
            token = new CancellationTokenSource();
        }

        private async void filter_changed(object sender, RoutedEventArgs e)
        {
            
            double d = 0;

            if (textBox.Text != null
                && double.TryParse(textBox.Text, out d))
            {
                var currentToken = new CancellationTokenSource();
                token.Cancel();
                token = currentToken;
                try
                {
                   var res = await Task.Run<IList<RandomCollection.SomeObject>>(() =>
                         {
                             System.Diagnostics.Debug.WriteLine("Task:{0}\tWith d:{1}\tStarted.", Task.CurrentId, d);
                             var result = (from o in new RandomCollection()
                                           where o.Value > d
                                           select o)
                                       .Take(25)
                                       .ToList();
                             if (currentToken.IsCancellationRequested)
                             {
                                 System.Diagnostics.Debug.WriteLine("Task:{0}\tWith d:{1}\tCanceled.", Task.CurrentId, d);
                                 return null;
                             }
                             {
                                 System.Diagnostics.Debug.WriteLine("Task:{0}\tWith d:{1}\tDone.", Task.CurrentId, d);
                                 return result;
                             }
                         }
                       , currentToken.Token);
                   if (res != null)
                       this.grid.ItemsSource = res;
                }
                catch (Exception ex) { }
                
            }
        }
    }
   
    public class RandomCollection
        : IEnumerable<RandomCollection.SomeObject>, IEnumerable
    {
        public class SomeObject
        {
            public SomeObject(Random r)
            {
                this.Value = r.NextDouble();
                this.Text = "";
                for (int i = r.Next(0, 16); i < 20; i++)
                    this.Text += "qqwertyioasdfghjk"[r.Next(0, 10)];
                //this is fucking slow
                Thread.Sleep(100);
            }
            public string Text { get; set; }
            public double Value { get; set; }
        }

        public IEnumerator<SomeObject> GetEnumerator()
        {
            Random r = new Random();
            var randomSize = r.Next(10000, 20000);
            for (int i = 0; i < randomSize; i++)
                yield return new SomeObject(r);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

       
    }
  
}
 
<Window x:Class="wpf_test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="299" Width="196" ItemsSource="{Binding Source=list}" x:Name ="grid" AutoGenerateColumns="false">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Text" Binding="{Binding Text}"/>
                <DataGridTextColumn Header="Value" Binding="{Binding Value}"/>
            </DataGrid.Columns>
        </DataGrid>
        <TextBox HorizontalAlignment="Left" Height="23" Margin="285,65,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="120" x:Name="textBox" TextChanged="filter_changed"/>

    </Grid>
</Window>
 

 

 

Δημοσ.

δες εδω

 

 

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace wpf_test
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        CancellationTokenSource token;
        public MainWindow()
        {
            InitializeComponent();
            token = new CancellationTokenSource();
        }

        private async void filter_changed(object sender, RoutedEventArgs e)
        {
            
            double d = 0;

            if (textBox.Text != null
                && double.TryParse(textBox.Text, out d))
            {
                var currentToken = new CancellationTokenSource();
                token.Cancel();
                token = currentToken;
                try
                {
                   var res = await Task.Run<IList<RandomCollection.SomeObject>>(() =>
                         {
                             System.Diagnostics.Debug.WriteLine("Task:{0}\tWith d:{1}\tStarted.", Task.CurrentId, d);
                             var result = (from o in new RandomCollection()
                                           where o.Value > d
                                           select o)
                                       .Take(25)
                                       .ToList();
                             if (currentToken.IsCancellationRequested)
                             {
                                 System.Diagnostics.Debug.WriteLine("Task:{0}\tWith d:{1}\tCanceled.", Task.CurrentId, d);
                                 return null;
                             }
                             {
                                 System.Diagnostics.Debug.WriteLine("Task:{0}\tWith d:{1}\tDone.", Task.CurrentId, d);
                                 return result;
                             }
                         }
                       , currentToken.Token);
                   if (res != null)
                       this.grid.ItemsSource = res;
                }
                catch (Exception ex) { }
                
            }
        }
    }
   
    public class RandomCollection
        : IEnumerable<RandomCollection.SomeObject>, IEnumerable
    {
        public class SomeObject
        {
            public SomeObject(Random r)
            {
                this.Value = r.NextDouble();
                this.Text = "";
                for (int i = r.Next(0, 16); i < 20; i++)
                    this.Text += "qqwertyioasdfghjk"[r.Next(0, 10)];
                //this is fucking slow
                Thread.Sleep(100);
            }
            public string Text { get; set; }
            public double Value { get; set; }
        }

        public IEnumerator<SomeObject> GetEnumerator()
        {
            Random r = new Random();
            var randomSize = r.Next(10000, 20000);
            for (int i = 0; i < randomSize; i++)
                yield return new SomeObject(r);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

       
    }
  
}
 
<Window x:Class="wpf_test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="299" Width="196" ItemsSource="{Binding Source=list}" x:Name ="grid" AutoGenerateColumns="false">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Text" Binding="{Binding Text}"/>
                <DataGridTextColumn Header="Value" Binding="{Binding Value}"/>
            </DataGrid.Columns>
        </DataGrid>
        <TextBox HorizontalAlignment="Left" Height="23" Margin="285,65,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="120" x:Name="textBox" TextChanged="filter_changed"/>

    </Grid>
</Window>
 

 

 

Μάλλον εδώ:

var currentToken = new CancellationTokenSource();

token.Cancel();

token = currentToken;

είναι όλο το ζουμί...
 
Παιδιά πραγματικά ευχαριστώ για την βοήθεια. Χώθηκα σε ένα side project τώρα, αλλά θα συνεχίσω με αυτό σε μερικές ώρες οπότε και θα επανέλθω με κώδικα!
Δημοσ.

Παίδες λύθηκε το πρόβλημα.. Αλλού ήταν το θέμα, έχω ένα linq query που υπό προϋποθέσεις έκανε 1000 χρόνια να γίνει ToList. Το έβαλα σε while loop με Skip & Take και δούλεψε smoothly.. Αν ενδιαφέρεται κανείς για κώδικα ας μου πει..

Δημοσ.

Όντος, ξέχασα να σου τονίσω ότι tolist το βάζεις στο τέλος, ώστε να μην μεταφέρεις όλα τα δεδομένα στη μνήμη.

 

Έχε το υποψιν ότι το linq με iqueryble εκτελείται εκεί που παίρνεις τα δεδομένα. Για αυτό παιρνε τα δεδομένα, εφόσον έχεις εφαρμόσει όλα τα φίλτρα σου.

Δημοσ.

   Γενικως πρεπει να εισαι προσεκτικος με τα linq queries, επειδη δεν εχεις αμεσο overview το πως εκτελειται, ειναι πολυ ευκολο, οπως και εκανες το παρεις ολα τα δεδομενα και μετα να τα φιλτραρεις. 

Δημοσ.

Όντος, ξέχασα να σου τονίσω ότι tolist το βάζεις στο τέλος, ώστε να μην μεταφέρεις όλα τα δεδομένα στη μνήμη.

 

Έχε το υποψιν ότι το linq με iqueryble εκτελείται εκεί που παίρνεις τα δεδομένα. Για αυτό παιρνε τα δεδομένα, εφόσον έχεις εφαρμόσει όλα τα φίλτρα σου.

 

   Γενικως πρεπει να εισαι προσεκτικος με τα linq queries, επειδη δεν εχεις αμεσο overview το πως εκτελειται, ειναι πολυ ευκολο, οπως και εκανες το παρεις ολα τα δεδομενα και μετα να τα φιλτραρεις. 

 

Το ήξερα ότι εκεί συσσορεύεται ο πόνος όλης της υπόθεσης, αλλά ήλπιζα ότι θα μπορέσω να "σκοτώσω" το thread, no matter what.. Με τον, ομολογουμένως πανέξυπνο (:P), τρόπο που σκέφτηκα δουλεύουν όλα ρολόι..

Δημιουργήστε ένα λογαριασμό ή συνδεθείτε για να σχολιάσετε

Πρέπει να είστε μέλος για να αφήσετε σχόλιο

Δημιουργία λογαριασμού

Εγγραφείτε με νέο λογαριασμό στην κοινότητα μας. Είναι πανεύκολο!

Δημιουργία νέου λογαριασμού

Σύνδεση

Έχετε ήδη λογαριασμό; Συνδεθείτε εδώ.

Συνδεθείτε τώρα
  • Δημιουργία νέου...