0

I have a grid whose (x,y) co-ordinates correspond to a SQL Server. There is not a row for every co-ordinate; only the ones that are not blank. There is a column ("fill") that stores these colour code integers.

When the application loads I have a sequence below that loops through every (X,Y) co-ordinate within the height/width of the grid. Within this loop, it asks whether the (X,Y) in question match those in the row and, if this returns TRUE, updates the Fill integer from that saved in the row. If this returns FALSE, I increase the row by one and look again, and again, until it has looked through every row saved in the table (named SQL.RecordCount). Once this has been done, I increase X (moving right, to the next cell) and do this process again; once I get to the end of the row (where X = MapWidth) I go to the start of the next row and go again. This repeats until Y = MapHeight, as shown in the code below.

To me this sequence makes sense, so there must be something I do not know is missing. Even if you do not have the exact solution, anything to move forward this gridlock would be very much appreciated.

enter image description here

Edits based on comments are in double asterix:

Public Sub LoadMap()

        Dim Fill As Integer = 0
        Dim Row As Integer = 0
        Dim X As Integer = 0
        Dim Y As Integer = 0
              

        SQL.ExecQuery("SELECT * FROM Mapper_Table")

        Do While Y <= MapHeight
            'Ultimate exit statement: stop loading cells when Y becomes greater than the MapHeight...
            Do While X <= MapWidth
                'Row Exit Statement: When X reaches 100, its time to start on the next row...
                Do While Row < SQL.RecordCount '12
                    'I only want to search as many rows as there are saved locations, otherwise the cell must be empty and therefore blank...
                    If X = SQL.DBDT.Rows(Row).Item("X") And Y = SQL.DBDT.Rows(Row).Item("Y") Then
                        'If the current (X,Y) is a match to the database row, then we take the stored colour...
                        Fill = SQL.DBDT.Rows(Row).Item("Fill")
                    End If
                    'If Fill is taken, we can colour the square; otherwise, we should look in the next row...
                     If Fill <> 0 Then
                    Map(X, Y, 0) = Fill
                    End If
          **  Row = Row + 1 **
                    Loop
                'Once we have looked in all the rows for out cell, it is either coloured in or not; we can move onto the next cell in the row; a.k.a...
                X = X + 1
          **  Fill = 0  **
            Loop
            'Once we have looked for all the cells in our row, we can move onto the next one; a.k.a...
            X = 0
            Y = Y + 1
        Loop
        'Once we get to the end of the map, it should be loaded!

    End Sub
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
  • Okay, thank you. Am I doing what you said though; querying all the data first and then having the sequence run through it? – Matthew Keracher Oct 20 '20 at 00:28
  • What is this `SQL` object? How does it work? Why aren't you using ADO.NET (`SqlConnection`, `SqlCommand`, and `SqlDataReader` directly?) – Dai Oct 20 '20 at 00:29
  • So SQL refers my SQL control class. I will update above... – Matthew Keracher Oct 20 '20 at 00:31
  • Why are you probing the result set for every X and Y coordinate on the grid? Why not just iterate over the result set and use the X, Y, Fill values it contains? – AlwaysLearning Oct 20 '20 at 00:32
  • Firstly - what is wrong with the loop? It goes forever? I'm not 100%, but if `FILL` has a value then the `Do While Row < SQL.RecordCount` loop will go forever? – seanb Oct 20 '20 at 00:32
  • @AlwaysLearning now we are talking! I guess I do not know how to do it from the other way around. Would this be done as an SQL language query or using VB? – Matthew Keracher Oct 20 '20 at 00:34
  • Another issue with the presented code is that the Fill variable never resets to 0. Once it gets a value it will continue to use that same value for all subsequent X and Y coordinates, or until it gets updated with a new Fill value from the result set. – AlwaysLearning Oct 20 '20 at 00:38
  • SeanB, AlwaysLearning afaik that loop ends when row reaches the number of saved rows in the datatable. I have set Fill = 0 above following your advice, though it still does not run! – Matthew Keracher Oct 20 '20 at 00:39
  • But the Row only gets updated if Fill = 0, otherwise it stays the same - so the next iteration is run on the same Row, which sets Fill, so Row isn't updated, etc. – seanb Oct 20 '20 at 00:42
  • OIC what your are saying; lemme see if I can tweak that and I will report back! – Matthew Keracher Oct 20 '20 at 00:43
  • @SeanB I have moved the Row increase out of the if statement that was dependent on the Fill value. It is still not working. I have edited it above, you can see. – Matthew Keracher Oct 20 '20 at 00:47

2 Answers2

1

Here's how I would do it, albiet in C# (converting this to VB.NET should be straightforward - I just really hate VB.NET's array syntax).

  • The code below works in 2 parts:

    • The first part (LoadMapperTableAsync) is only concerned with loading data from your Mapper_table into program-memory (into a List<T>, where it uses ValueTuples to represent your table data).
    • The second part (ConvertTo2DArray) converts the loaded data into a 2D array of colors.
  • It does this with 2 consecutive loops (for O(n) runtime complexity):

    • The first loop is in LoadMapperTableAsync as it iterates over the rows from the SqlDataReader.
    • The second loop is in ConvertTo2DArray and is simply concerned with populating the array.
      • When you create a new array in .NET, the CLR will use pre-zeroed memory, so all T[] array elements automatically have the value default(T). In this case, the default for FillColor is None, which I assume is what you want.
      • Remember that setting array elements by index is a O(1) operation.
  • The two loops could be combined, but I strongly recommend against it because then you're mixing dumb and straightforward data loading code with application-logic (aka business-logic or domain-logic) which is generally a bad thing because it makes program maintenance much harder. I appreciate you're new to this, but there are no hard-and-fast rules, but I'll say that with increasing experience you'll get a "feel" for what behaviour belongs in what parts of your program and what parts should be kept separate from other parts.

public enum FillColor {
    None = 0, // It's important that zero represents "empty" or "not set" due to .NET's rules on default values for value-types like enums.
    Red = 1,
    Blue = 2,
    // etc
}

public static async Task< List<( Int32 x, Int32 y, FillColor fill )> > LoadMapperTableAsync()
{
    const String connectionString = "...";

    using( SqlConnection c = new SqlConnection( connectionString ) )
    using( SqlCommand cmd = c.CreateCommand() )
    {
        await c.OpenAsync().ConfigureAwait(false);

        cmd.CommandText = "SELECT x, y, fill FROM Mapper_Table"; // NEVER EVER USE `SELECT * FROM` in application code!!!!!! Always explicitly name your columns!
        
        using( SqlDataReader rdr = await cmd.ExecuteReaderAsync().ConfigureAwait(false) )
        {
            List<( Int32 x, Int32 y, FillColor fill )> list = new List<( Int32 x, Int32 y, FillColor fill )>();
            
            while( await rdr.ReadAsync().ConfigureAwait(false) )
            {
                Int32 x    = rdr.GetInt32( 0 ); // Add `rdr.IsDBNull()` guards, if necessary!
                Int32 y    = rdr.GetInt32( 1 );
                Int32 fill = rdr.GetInt32( 2 );

                list.Add( ( x, y, (FillColor)fill ) );
            }

            return list;
        }
    }
}

public static FillColor[,] ConvertTo2DArray( IEnumerable< ( Int32 x, Int32 y, FillColor fill ) > values )
{
    const Int32 MAP_WIDTH  = 100;
    const Int32 MAP_HEIGHT = 100;

    FillColor[,] map = new FillColor[ MAP_HEIGHT, MAP_WIDTH ];

    // Do note that 2D arrays in .NET are slooooow: https://stackoverflow.com/questions/468832/why-are-multi-dimensional-arrays-in-net-slower-than-normal-arrays

    foreach( ( Int32 x, Int32 y, FillColor fill ) in values )
    {
        // Skip-over invalid data:
        if( x < 0 || x > MAP_WIDTH  ) continue;
        if( y < 0 || y > MAP_HEIGHT ) continue;

        map[ y, x ] = fill;
    }

    return map;
}

public static async Task<FillColor[,]> LoadAsync()
{
    List<( Int32 x, Int32 y, FillColor fill )> list = await LoadMapperTableAsync().ConfigureAwait(false);

    return ConvertTo2DArray( list );
}
Dai
  • 141,631
  • 28
  • 261
  • 374
  • Thankyou for taking the time to type this out @Dai, I will work on it and get back to you! – Matthew Keracher Oct 20 '20 at 01:01
  • Why `.ConfigureAwait(false)` here? It looks like a WinForms project. – Jimi Oct 20 '20 at 01:06
  • @Jimi Correct. I added it speciifcally *because* it's WinForms: this is because `LoadMapperTableAsync` has a _tight loop_ in it (specifically the `while( await rdr.ReadAsync )` loop) you really don't want the continuation resuming on the UI thread: you want it to resume on the _first_ available thread-pool thread, hence `ConfigureAwait(false)`. No UI controls or objects are used inside the method so `ConfigureAwait(false)` will result in the best performance. – Dai Oct 20 '20 at 01:08
  • Dependable. How is the OP calling `LoadAsync()`? `ConfigureAwait(false)` may be useful here, because of `ConvertTo2DArray(list);`: If this was run in a threadpool thread, you wouldn't need it (I'm not actually proposing a different method, it's something for the OP to think/test about). Drawing that grid may take 20-30ms, when the data is ready. – Jimi Oct 20 '20 at 01:19
0

enter image description here

Public Sub LoadMap()

    Dim Fill As Integer = 0
    Dim Row As Integer = 0
    Dim X As Integer = 0
    Dim Y As Integer = 0

    'Experiment: Sets X and Y to CurrentLocation with Record that has Fill. Works
    'Experiment: If Row(0) is not correct, try other rows. 
    'Experiment: Loop the loop for all X and Y's


    SQL.ExecQuery("SELECT X,Y,Fill FROM Mapper_Table")

    Do While Y <= MapHeight
        'Ultimate exit statement: stop loading cells when Y becomes greater than the MapHeight...
        Do While X <= MapWidth
            'Row Exit Statement: When X reaches 100, its time to start on the next row...
            Do While Row < SQL.RecordCount
                'I only want to search as many rows as there are saved locations, otherwise the cell must be empty and therefore blank...
                If X = SQL.DBDT.Rows(Row).Item("X") And Y = SQL.DBDT.Rows(Row).Item("Y") Then
                    'If the current (X,Y) is a match to the database row, then we take the stored colour...
                    Fill = SQL.DBDT.Rows(Row).Item("Fill")
                    'And paint the cell...
                    Map(X, Y, 0) = Fill
                    Row = SQL.RecordCount
                    'Otherwise, we look at the next row
                Else Row = Row + 1
                End If
            Loop
            'Once we have looked in all the rows for out cell, it is either coloured in or not; we can move onto the next cell in the row; a.k.a...
            X = X + 1
            Row = 0
            Fill = 0
        Loop
        'Once we have looked for all the cells in our row, we can move onto the next one; a.k.a...
        X = 0
        Y = Y + 1
    Loop
    'Once we get to the end of the map, it should be loaded!

End Sub